Compare commits

...

96 Commits

Author SHA1 Message Date
Paulus Schoutsen
b3454bfd9c 2023.1.2 (#85481) 2023-01-08 21:44:13 -05:00
Paulus Schoutsen
834847988d Bumped version to 2023.1.2 2023-01-08 20:24:25 -05:00
Allen Porter
caf15534bb Bump gcal_sync to 4.1.1 (#85453) 2023-01-08 20:24:19 -05:00
Allen Porter
10cb2e31c4 Bump ical to 4.2.9 (#85401) 2023-01-08 20:24:18 -05:00
Lutz Lengemann
85c9f9facf Increase Hydrawise default scan interval (#85398)
Increasing default scan interval

Fixes #83540
2023-01-08 20:24:17 -05:00
J. Nick Koston
5ff7b3bb1a Bump pySwitchbot to 0.36.3 (#85360) 2023-01-08 20:24:16 -05:00
J. Nick Koston
e5ba423d6d Add note to SwitchBot locks that usernames are case sensitive (#85359) 2023-01-08 20:24:15 -05:00
puddly
b30d4ef7cf Bump ZHA dependencies (#85355)
* Bump ZHA dependencies

* Deprecated `foundation.Command` -> `foundation.GeneralCommand`
2023-01-08 20:24:15 -05:00
Joakim Plate
00e563f1b8 Switch play pause method in philips js (#85343)
fixes undefined
2023-01-08 20:24:14 -05:00
Phil Bruckner
cf06f3b81d Bump life360 package to 5.5.0 (#85322)
Improve debug output & redact sensitive info from log.
Fix bug that was masking some HTTP errors.
Retry HTTP errors 502, 503 & 504, which have been observed to happen every once
in a while, resulting in fewer unnecessary unavailable states.
2023-01-08 20:24:13 -05:00
starkillerOG
a781fcca86 Bump reolink-aio to 0.1.3 (#85309)
bump reolink-aio to 0.1.3
2023-01-08 20:24:12 -05:00
Tom Puttemans
764550f2e1 Fix dsmr_reader peak hour consumption unit of measurement (#85301) 2023-01-08 20:24:11 -05:00
puddly
7396bcc585 Retry ZHA config entry setup when ENETUNREACH is caught (#84615)
* The config entry is not ready on `ENETUNREACH`

* Use new `TransientConnectionError` from zigpy
2023-01-08 20:20:26 -05:00
epenet
7e6b087773 Allow SensorDeviceClass.POWER_FACTOR unit None (#85287)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-01-06 09:27:02 +01:00
Paulus Schoutsen
71ce7373a3 2023.1.1 (#85277) 2023-01-05 23:18:21 -05:00
Paulus Schoutsen
33bb9c230b Bumped version to 2023.1.1 2023-01-05 22:24:17 -05:00
Charles Garwood
f0f2c12d91 Fix Fully Kiosk service call config entry handling (#85275)
* Make sure we're getting the fully_kiosk config entry

* Make sure we're getting the fully_kiosk config entry
2023-01-05 22:23:52 -05:00
J. Nick Koston
2840821594 Reject the WiFI AP when considering to update a shelly config entry from zeroconf (#85265)
Reject the WiFI AP IP when considering to update a shelly config entry from zeroconf

fixes #85180
2023-01-05 22:23:51 -05:00
rikroe
edfd83c3a7 Bump bimmer_connected to 0.12.0 (#85255)
* Bump bimmer_connected to 0.12.0

* Fix mypy

* Remove not needed code

Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2023-01-05 22:23:51 -05:00
starkillerOG
ee88f34a91 bump reolink-aio to 0.1.2 (#85247) 2023-01-05 22:22:35 -05:00
J. Nick Koston
fa4c250001 Improve error reporting when switchbot auth fails (#85244)
* Improve error reporting when switchbot auth fails

related issue #85243

* bump

* coverage
2023-01-05 22:21:55 -05:00
Franck Nijhof
59d6f827c3 Fix device class for DSMR gas sensors providing energy readings (#85202) 2023-01-05 22:18:30 -05:00
epenet
26ea02aa8f Remove invalid device class for RSSI sensors (#85191)
* Remove invalid device class for RRSI sensors

* Restore state class
2023-01-05 22:18:29 -05:00
epenet
d73b86132b Adjust valid energy units (#85190) 2023-01-05 22:18:28 -05:00
Franck Nijhof
8034faadca Remove invalid AQI unit from Environment Canada (#85183) 2023-01-05 22:18:27 -05:00
Erik Montnemery
3c2b7c0d69 Bump hatasmota to 0.6.2 (#85182) 2023-01-05 22:18:26 -05:00
Ernst Klamer
563ad02c65 Bump bthome-ble to 2.4.1 (#85153)
fix https://github.com/home-assistant/core/issues/85142
fixes undefined
2023-01-05 22:15:13 -05:00
Michal Čihař
fe89b663e7 Fix lacrosse_view fetching of latest data (#85117)
lacrosse_view: fixed fetching of latest data

When using datetime.utcnow(), it only replaces timezone information with
UTC making the actual time offset by the timezone. When you are in UTC-
timezones, it makes no issue as the offset is in the future, but when in
UTC+, the last hour(s) of data are missing.

This commits swtiches to time.time() as UTC timestamp is actually what
the API expects.

It also reduces the window to one hour what noticeably improves the API
performance.
2023-01-05 22:15:12 -05:00
William Scanlon
dcd07d3135 Bump pyeconet to 0.1.18 to fix energy usage (#85094) 2023-01-05 22:15:11 -05:00
Robert Svensson
8bf2299407 Only subscribe to relevant IDs for state updates (#85252)
Make sure to only subscribe to the relevant ID
2023-01-05 21:49:22 -05:00
Robert Svensson
9c689d757c Limit calls in UniFi to write state (#85248)
Limit calls to write state to when relevant
2023-01-05 15:38:24 -05:00
Franck Nijhof
4e4fc1767f 2023.1.0 (#85120) 2023-01-04 20:47:22 +01:00
Martin Hjelmare
cc3c5772c5 Fix Z-Wave JS sensor units and device classes (#85129)
fixes undefined
2023-01-04 19:47:59 +01:00
J. Nick Koston
6ba6991ecd Bump home-assistant-bluetooth to 1.9.2 (#85123) 2023-01-04 18:36:15 +01:00
Franck Nijhof
d52d068469 Merge branch 'master' into rc 2023-01-04 17:09:15 +01:00
Franck Nijhof
09b3611a63 Bumped version to 2023.1.0 2023-01-04 17:05:21 +01:00
Joakim Sørensen
ab2f05d3e9 Handle zone exception when setting up Cloudflare (#85110) 2023-01-04 17:04:43 +01:00
Guido Schmitz
90ac0c870f Remove illuminance device class for sensors in devolo Home Control (#85108) 2023-01-04 17:04:40 +01:00
Bram Kragten
0fd113db59 Update frontend to 20230104.0 (#85107) 2023-01-04 17:04:35 +01:00
Jan Bouwhuis
1b43323f5e Allow MQTT device_class or state_class to be set as None (#85106)
* Allow MQTT device_class to be set as `None`

* Add test

* Also allow sensor state_class to be `None`
2023-01-04 17:04:30 +01:00
Jan Bouwhuis
6108e581b1 Do not reset current selection on reconfig or MQTT select (#85099)
* Do not reset current selection on reconfig

* Add a test
2023-01-04 11:54:27 +01:00
Aaron Bach
c8c68f05ec Remove workaround for reloading PurpleAir upon device removal (#85086) 2023-01-04 11:53:06 +01:00
Franck Nijhof
b80467dc58 Update adguard to 0.6.1 (#85052)
* Update adguard to 0.6.0

* Update adguard to 0.6.1
2023-01-04 11:49:16 +01:00
Andre Lengwenus
6e9f0eca03 Fix integer only LCN variable values (#85035) 2023-01-04 11:49:10 +01:00
Paulus Schoutsen
cc6a2f0338 2022.12.9 (#85030) 2023-01-02 22:06:36 -05:00
J. Nick Koston
6ebf2ec9ec Fix failing HomeKit Controller diagnostics tests (#84936) 2023-01-02 22:05:25 -05:00
Paulus Schoutsen
9ecee11af6 Bumped version to 2023.1.0b5 2023-01-02 21:30:16 -05:00
J. Nick Koston
9a1669103b Fix bluetooth not being loaded with esphome proxies when removed from default_config (#85032)
* Fix bluetooth not being loaded with esphome proxies when removed from default_config

fixes #84960

* actually commit the conftest change
2023-01-02 21:30:02 -05:00
Paulus Schoutsen
368ea0586d Bump slixmpp to 1.8.3 (#85031) 2023-01-02 21:30:01 -05:00
J. Nick Koston
4a7db6ee71 Bump httpx to 0.23.2 (#85023)
changelogs:
https://github.com/encode/httpcore/compare/0.16.2...0.16.3
https://github.com/encode/httpx/compare/0.23.1...0.23.2
2023-01-02 21:30:00 -05:00
Tobias Sauerwein
a10b9572c7 Bump pyatmo to v7.5.0 (#85016) 2023-01-02 21:29:59 -05:00
starkillerOG
0b47bf1f0b Switch to reolink-aio (#85014)
* switch to reolink-aio

* fix imports
2023-01-02 21:29:58 -05:00
Bram Kragten
5f4d286556 Update frontend to 20230102.0 (#85010)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-01-02 21:29:57 -05:00
Daniel Hjelseth Høyer
b23ab3c65a Update Tibber lib to 0.26.7. Improve error handling of realtime data (#85008) 2023-01-02 21:29:56 -05:00
J. Nick Koston
7c199b36f8 Bump home-assistant-bluetooth to 1.9.1 (#85005)
fixes #83722
2023-01-02 21:29:55 -05:00
David F. Mulcahey
d4e55ee030 Bump ZHA quirks (#85004) 2023-01-02 21:29:55 -05:00
starkillerOG
f3ec82543e Bump motionblinds to 0.6.15 (#84994) 2023-01-02 21:29:54 -05:00
Erik Montnemery
4013d4c48d Revert "Add aliases to device registry items" (#84976) 2023-01-02 21:29:53 -05:00
Martin Hjelmare
93ac908776 Handle not available add-on in hassio add-on manager (#84943)
* Handle not available add-on in hassio add-on manager

* Fix zwave_js tests

* Fix sky connect tests

* Fix matter tests

* Fix yellow tests

* Update hardware tests
2023-01-02 21:29:52 -05:00
starkillerOG
2ad1a53038 Consider 95% as closed for Motion blinds venetian blinds (#84872) 2023-01-02 21:29:51 -05:00
Paulus Schoutsen
3ba59fbebe Bumped version to 2022.12.9 2023-01-02 20:30:09 -05:00
Martin Hjelmare
f3fab5c1f5 Handle not available add-on in hassio add-on manager (#84943)
* Handle not available add-on in hassio add-on manager

* Fix zwave_js tests

* Fix sky connect tests

* Fix matter tests

* Fix yellow tests

* Update hardware tests
2023-01-02 20:30:03 -05:00
Paulus Schoutsen
2d120cb6ba Bumped version to 2023.1.0b4 2023-01-01 20:25:29 -05:00
Allen Porter
ad782166c7 Fix caldav calendars with custom timezones (#84955)
* Fix caldav calendars with custom timezones

* Revert whitespace change
2023-01-01 20:23:33 -05:00
J. Nick Koston
bc9202cf02 Bump pySwitchbot to 0.36.1 (#84937)
changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.36.0...0.36.1

small fix for the battery not updating with passive scanning
after lock operation
2023-01-01 20:23:32 -05:00
J. Nick Koston
0d385d3b67 Fix failing HomeKit Controller diagnostics tests (#84936) 2023-01-01 20:23:31 -05:00
Allen Porter
76fa24aba1 Fix Climate device HVAC mode trigger UI (#84930)
* Fix Climate device HVAC mode trigger UI

* Use updated order of test case results
2023-01-01 20:23:30 -05:00
ChopperRob
95ae37cd87 Fix haveibeenpwned user-agent string (#84919)
* Fixed user-agent string not being accepted as an valid header

* Update homeassistant/components/haveibeenpwned/sensor.py

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

* Removed the aiohttp import

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-01-01 20:22:56 -05:00
Robert Svensson
bc1d22f4ec Bump pydeconz to v106 (#84914)
fixes undefined
2023-01-01 20:20:15 -05:00
Allen Porter
67e1872ab6 Fix free/busy google calendars (#84907)
fixes undefined
2023-01-01 20:20:14 -05:00
tronikos
516c2b0cdb Google Assistant SDK: Log command and response (#84904)
Log command and response
2023-01-01 20:20:13 -05:00
Aaron Bach
60f067b68f Use serial number for AirVisal Pro config entry unique ID (#84902)
* Use serial number for AirVisal Pro config entry unique ID

* Code review
2023-01-01 20:20:13 -05:00
Aaron Bach
ff76567061 Fix issues with PurpleAir sensor device class and unit (#84896) 2023-01-01 20:20:12 -05:00
Aaron Bach
93488cfa0f Don't include distance in PurpleAir sensor selector (#84893) 2023-01-01 20:20:11 -05:00
J. Nick Koston
9655619667 Small fixes for SwitchBot Locks (#84888)
Co-authored-by: Aaron Bach <bachya1208@gmail.com>
2023-01-01 20:20:10 -05:00
starkillerOG
32736b3336 Process late feedback for Reolink (#84884)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-01-01 20:20:09 -05:00
starkillerOG
c77b78928e Bump motionblinds to 0.6.14 (#84873) 2023-01-01 20:20:08 -05:00
William Scanlon
a7ba242f1f Bump pyeconet to 0.1.17 (#84868) 2023-01-01 20:20:08 -05:00
Greg Dowling
043d58d697 Bump pyroon library to 0.1.2. (#84865) 2023-01-01 20:20:07 -05:00
Robert Svensson
6408890543 Bump aiounifi to v43 (#84864) 2023-01-01 20:20:06 -05:00
Franck Nijhof
c5f7d7ae85 Only reflect unavailable state in DSMR when disconnected (#84862)
* Only reflect unavailable state in DSMR when disonnected

* Addressreview comment
2023-01-01 20:20:05 -05:00
Franck Nijhof
7ab27cd9bf Do not validate device classes when entity state is unknown (#84860) 2023-01-01 20:20:04 -05:00
Artem Draft
9932c0cb91 Bump pybravia to 0.2.5 (#84835) 2023-01-01 20:20:03 -05:00
Aaron Bach
565d4f85c1 Ensure AirVisual Pro migration includes device and entity customizations (#84798)
* Ensure AirVisual Pro migration includes device and entity customizations

* Update homeassistant/components/airvisual/__init__.py

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

* Code review

* Fix tests

* Fix tests FOR REAL

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-01-01 20:20:02 -05:00
Franck Nijhof
7be60d4569 Bumped version to 2023.1.0b3 2022-12-30 16:50:35 +01:00
Franck Nijhof
a50622cbfd Add availability property to DSMR sensors (#84848) 2022-12-30 16:49:35 +01:00
Bram Kragten
fb41b024c0 Update frontend to 20221230.0 (#84842) 2022-12-30 16:49:31 +01:00
Artem Draft
80ac4c0269 Redesign and refactor Bravia TV config_flow (#84832)
fixes undefined
2022-12-30 16:49:27 +01:00
Damian Sypniewski
0e0677b690 Add option to retrieve SwitchBot Lock encryption key through config flow (#84830)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-12-30 16:48:39 +01:00
SukramJ
50d9e3efe6 Add mA to SensorDeviceClass.CURRENT units (#84492)
fixes undefined
2022-12-30 16:45:39 +01:00
Jan Bouwhuis
ca28006d76 Add mV as a unit for voltage and enable conversions (#84805)
fixes undefined
2022-12-30 16:41:51 +01:00
Phil Cole
ac3711e6ab Use pycarwings2 2.14 (#84792)
fixes undefined
2022-12-30 16:40:22 +01:00
epenet
5901964bf6 Enable unit conversion for DATA_SIZE (#84699) 2022-12-30 16:40:19 +01:00
epenet
b24c40f2df Enable unit conversion for DATA_RATE (#84698) 2022-12-30 16:40:16 +01:00
Chris Straffon
2cb7a80f98 Fix growatt identification issue (#84628)
Fixes https://github.com/home-assistant/core/issues/84600
fixes undefined
2022-12-30 16:40:11 +01:00
Steven Looman
f05de2b28c Actually try port when finding next available port for ssdp server (#84206)
fixes undefined
2022-12-30 16:29:17 +01:00
135 changed files with 1940 additions and 945 deletions

View File

@@ -947,8 +947,8 @@ build.json @home-assistant/supervisor
/tests/components/remote/ @home-assistant/core
/homeassistant/components/renault/ @epenet
/tests/components/renault/ @epenet
/homeassistant/components/reolink/ @starkillerOG @JimStar
/tests/components/reolink/ @starkillerOG @JimStar
/homeassistant/components/reolink/ @starkillerOG
/tests/components/reolink/ @starkillerOG
/homeassistant/components/repairs/ @home-assistant/core
/tests/components/repairs/ @home-assistant/core
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther

View File

@@ -3,7 +3,7 @@
"name": "AdGuard Home",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adguard",
"requirements": ["adguardhome==0.5.1"],
"requirements": ["adguardhome==0.6.1"],
"codeowners": ["@frenck"],
"iot_class": "local_polling",
"integration_type": "service",

View File

@@ -32,6 +32,7 @@ from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -117,36 +118,6 @@ def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str:
)
@callback
def async_get_pro_config_entry_by_ip_address(
hass: HomeAssistant, ip_address: str
) -> ConfigEntry:
"""Get the Pro config entry related to an IP address."""
[config_entry] = [
entry
for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO)
if entry.data[CONF_IP_ADDRESS] == ip_address
]
return config_entry
@callback
def async_get_pro_device_by_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dr.DeviceEntry:
"""Get the Pro device entry related to a config entry.
Note that a Pro config entry can only contain a single device.
"""
device_registry = dr.async_get(hass)
[device_entry] = [
device_entry
for device_entry in device_registry.devices.values()
if config_entry.entry_id in device_entry.config_entries
]
return device_entry
@callback
def async_sync_geo_coordinator_update_intervals(
hass: HomeAssistant, api_key: str
@@ -306,14 +277,31 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
version = 3
if entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_NODE_PRO:
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
ip_address = entry.data[CONF_IP_ADDRESS]
# Get the existing Pro device entry before it is removed by the migration:
old_device_entry = async_get_pro_device_by_config_entry(hass, entry)
# Store the existing Pro device before the migration removes it:
old_device_entry = next(
entry
for entry in dr.async_entries_for_config_entry(
device_registry, entry.entry_id
)
)
# Store the existing Pro entity entries (mapped by unique ID) before the
# migration removes it:
old_entity_entries: dict[str, er.RegistryEntry] = {
entry.unique_id: entry
for entry in er.async_entries_for_device(
entity_registry, old_device_entry.id, include_disabled_entities=True
)
}
# Remove this config entry and create a new one under the `airvisual_pro`
# domain:
new_entry_data = {**entry.data}
new_entry_data.pop(CONF_INTEGRATION_TYPE)
tasks = [
hass.config_entries.async_remove(entry.entry_id),
hass.config_entries.flow.async_init(
@@ -324,18 +312,52 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
]
await asyncio.gather(*tasks)
# After the migration has occurred, grab the new config and device entries
# (now under the `airvisual_pro` domain):
new_config_entry = next(
entry
for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO)
if entry.data[CONF_IP_ADDRESS] == ip_address
)
new_device_entry = next(
entry
for entry in dr.async_entries_for_config_entry(
device_registry, new_config_entry.entry_id
)
)
# Update the new device entry with any customizations from the old one:
device_registry.async_update_device(
new_device_entry.id,
area_id=old_device_entry.area_id,
disabled_by=old_device_entry.disabled_by,
name_by_user=old_device_entry.name_by_user,
)
# Update the new entity entries with any customizations from the old ones:
for new_entity_entry in er.async_entries_for_device(
entity_registry, new_device_entry.id, include_disabled_entities=True
):
if old_entity_entry := old_entity_entries.get(
new_entity_entry.unique_id
):
entity_registry.async_update_entity(
new_entity_entry.entity_id,
area_id=old_entity_entry.area_id,
device_class=old_entity_entry.device_class,
disabled_by=old_entity_entry.disabled_by,
hidden_by=old_entity_entry.hidden_by,
icon=old_entity_entry.icon,
name=old_entity_entry.name,
new_entity_id=old_entity_entry.entity_id,
unit_of_measurement=old_entity_entry.unit_of_measurement,
)
# If any automations are using the old device ID, create a Repairs issues
# with instructions on how to update it:
if device_automations := automation.automations_with_device(
hass, old_device_entry.id
):
new_config_entry = async_get_pro_config_entry_by_ip_address(
hass, ip_address
)
new_device_entry = async_get_pro_device_by_config_entry(
hass, new_config_entry
)
async_create_issue(
hass,
DOMAIN,

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Any
from pyairvisual.node import (
@@ -33,13 +34,24 @@ STEP_USER_SCHEMA = vol.Schema(
)
async def async_validate_credentials(ip_address: str, password: str) -> dict[str, Any]:
"""Validate an IP address/password combo (and return any errors as appropriate)."""
@dataclass
class ValidationResult:
"""Define a validation result."""
serial_number: str | None = None
errors: dict[str, Any] = field(default_factory=dict)
async def async_validate_credentials(
ip_address: str, password: str
) -> ValidationResult:
"""Validate an IP address/password combo."""
node = NodeSamba(ip_address, password)
errors = {}
try:
await node.async_connect()
measurements = await node.async_get_latest_measurements()
except InvalidAuthenticationError as err:
LOGGER.error("Invalid password for Pro at IP address %s: %s", ip_address, err)
errors["base"] = "invalid_auth"
@@ -52,10 +64,12 @@ async def async_validate_credentials(ip_address: str, password: str) -> dict[str
except Exception as err: # pylint: disable=broad-except
LOGGER.exception("Unknown error while connecting to %s: %s", ip_address, err)
errors["base"] = "unknown"
else:
return ValidationResult(serial_number=measurements["serial_number"])
finally:
await node.async_disconnect()
return errors
return ValidationResult(errors=errors)
class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -89,11 +103,15 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
assert self._reauth_entry
if errors := await async_validate_credentials(
validation_result = await async_validate_credentials(
self._reauth_entry.data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]
):
)
if validation_result.errors:
return self.async_show_form(
step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA, errors=errors
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
errors=validation_result.errors,
)
self.hass.config_entries.async_update_entry(
@@ -113,14 +131,18 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
ip_address = user_input[CONF_IP_ADDRESS]
await self.async_set_unique_id(ip_address)
self._abort_if_unique_id_configured()
if errors := await async_validate_credentials(
validation_result = await async_validate_credentials(
ip_address, user_input[CONF_PASSWORD]
):
)
if validation_result.errors:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors
step_id="user",
data_schema=STEP_USER_SCHEMA,
errors=validation_result.errors,
)
await self.async_set_unique_id(validation_result.serial_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=ip_address, data=user_input)

View File

@@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
return (
self.vehicle.vehicle_location.location[0]
if self.vehicle.is_vehicle_tracking_enabled
and self.vehicle.vehicle_location.location
else None
)
@@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
return (
self.vehicle.vehicle_location.location[1]
if self.vehicle.is_vehicle_tracking_enabled
and self.vehicle.vehicle_location.location
else None
)

View File

@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.10.4"],
"requirements": ["bimmer_connected==0.12.0"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling",

View File

@@ -44,8 +44,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.client: BraviaTV | None = None
self.device_config: dict[str, Any] = {}
self.entry: ConfigEntry | None = None
self.client_id: str = ""
self.nickname: str = ""
@staticmethod
@callback
@@ -62,8 +60,13 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
self.client = BraviaTV(host=host, session=session)
async def async_create_device(self) -> FlowResult:
"""Initialize and create Bravia TV device from config."""
async def gen_instance_ids(self) -> tuple[str, str]:
"""Generate client_id and nickname."""
uuid = await instance_id.async_get(self.hass)
return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}"
async def async_connect_device(self) -> None:
"""Connect to Bravia TV device from config."""
assert self.client
pin = self.device_config[CONF_PIN]
@@ -72,13 +75,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if use_psk:
await self.client.connect(psk=pin)
else:
self.device_config[CONF_CLIENT_ID] = self.client_id
self.device_config[CONF_NICKNAME] = self.nickname
await self.client.connect(
pin=pin, clientid=self.client_id, nickname=self.nickname
)
client_id = self.device_config[CONF_CLIENT_ID]
nickname = self.device_config[CONF_NICKNAME]
await self.client.connect(pin=pin, clientid=client_id, nickname=nickname)
await self.client.set_wol_mode(True)
async def async_create_device(self) -> FlowResult:
"""Create Bravia TV device from config."""
assert self.client
await self.async_connect_device()
system_info = await self.client.get_system_info()
cid = system_info[ATTR_CID].lower()
title = system_info[ATTR_MODEL]
@@ -90,6 +96,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=title, data=self.device_config)
async def async_reauth_device(self) -> FlowResult:
"""Reauthorize Bravia TV device from config."""
assert self.entry
assert self.client
await self.async_connect_device()
self.hass.config_entries.async_update_entry(self.entry, data=self.device_config)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -100,28 +116,51 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
host = user_input[CONF_HOST]
if is_host_valid(host):
self.device_config[CONF_HOST] = host
self.create_client()
return await self.async_step_authorize()
errors[CONF_HOST] = "invalid_host"
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST, default=""): str}),
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors,
)
async def async_step_authorize(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Authorize Bravia TV device."""
"""Handle authorize step."""
self.create_client()
if user_input is not None:
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
if user_input[CONF_USE_PSK]:
return await self.async_step_psk()
return await self.async_step_pin()
return self.async_show_form(
step_id="authorize",
data_schema=vol.Schema(
{
vol.Required(CONF_USE_PSK, default=False): bool,
}
),
)
async def async_step_pin(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle PIN authorize step."""
errors: dict[str, str] = {}
self.client_id, self.nickname = await self.gen_instance_ids()
client_id, nickname = await self.gen_instance_ids()
if user_input is not None:
self.device_config[CONF_PIN] = user_input[CONF_PIN]
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
self.device_config[CONF_CLIENT_ID] = client_id
self.device_config[CONF_NICKNAME] = nickname
try:
if self.entry:
return await self.async_reauth_device()
return await self.async_create_device()
except BraviaTVAuthError:
errors["base"] = "invalid_auth"
@@ -133,16 +172,44 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self.client
try:
await self.client.pair(self.client_id, self.nickname)
await self.client.pair(client_id, nickname)
except BraviaTVError:
return self.async_abort(reason="no_ip_control")
return self.async_show_form(
step_id="authorize",
step_id="pin",
data_schema=vol.Schema(
{
vol.Required(CONF_PIN, default=""): str,
vol.Required(CONF_USE_PSK, default=False): bool,
vol.Required(CONF_PIN): str,
}
),
errors=errors,
)
async def async_step_psk(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle PSK authorize step."""
errors: dict[str, str] = {}
if user_input is not None:
self.device_config[CONF_PIN] = user_input[CONF_PIN]
try:
if self.entry:
return await self.async_reauth_device()
return await self.async_create_device()
except BraviaTVAuthError:
errors["base"] = "invalid_auth"
except BraviaTVNotSupported:
errors["base"] = "unsupported_model"
except BraviaTVError:
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="psk",
data_schema=vol.Schema(
{
vol.Required(CONF_PIN): str,
}
),
errors=errors,
@@ -181,7 +248,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Allow the user to confirm adding the device."""
if user_input is not None:
self.create_client()
return await self.async_step_authorize()
return self.async_show_form(step_id="confirm")
@@ -190,59 +256,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle configuration by re-auth."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self.device_config = {**entry_data}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Dialog that informs the user that reauth is required."""
self.create_client()
client_id, nickname = await self.gen_instance_ids()
assert self.client is not None
assert self.entry is not None
if user_input is not None:
pin = user_input[CONF_PIN]
use_psk = user_input[CONF_USE_PSK]
try:
if use_psk:
await self.client.connect(psk=pin)
else:
self.device_config[CONF_CLIENT_ID] = client_id
self.device_config[CONF_NICKNAME] = nickname
await self.client.connect(
pin=pin, clientid=client_id, nickname=nickname
)
await self.client.set_wol_mode(True)
except BraviaTVError:
return self.async_abort(reason="reauth_unsuccessful")
else:
self.hass.config_entries.async_update_entry(
self.entry, data={**self.device_config, **user_input}
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
try:
await self.client.pair(client_id, nickname)
except BraviaTVError:
return self.async_abort(reason="reauth_unsuccessful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PIN, default=""): str,
vol.Required(CONF_USE_PSK, default=False): bool,
}
),
)
async def gen_instance_ids(self) -> tuple[str, str]:
"""Generate client_id and nickname."""
uuid = await instance_id.async_get(self.hass)
return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}"
return await self.async_step_authorize()
class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):

View File

@@ -2,7 +2,7 @@
"domain": "braviatv",
"name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv",
"requirements": ["pybravia==0.2.3"],
"requirements": ["pybravia==0.2.5"],
"codeowners": ["@bieniu", "@Drafteed"],
"ssdp": [
{

View File

@@ -9,21 +9,27 @@
},
"authorize": {
"title": "Authorize Sony Bravia TV",
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.",
"description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
"data": {
"pin": "[%key:common::config_flow::data::pin%]",
"use_psk": "Use PSK authentication"
}
},
"pin": {
"title": "Authorize Sony Bravia TV",
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.",
"data": {
"pin": "[%key:common::config_flow::data::pin%]"
}
},
"psk": {
"title": "Authorize Sony Bravia TV",
"description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set «Authentication» to «Normal and Pre-Shared Key» or «Pre-Shared Key» and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.",
"data": {
"pin": "PSK"
}
},
"confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
},
"reauth_confirm": {
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.",
"data": {
"pin": "[%key:common::config_flow::data::pin%]",
"use_psk": "Use PSK authentication"
}
}
},
"error": {
@@ -36,8 +42,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_ip_control": "IP Control is disabled on your TV or the TV is not supported.",
"not_bravia_device": "The device is not a Bravia TV.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again."
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {

View File

@@ -4,8 +4,7 @@
"already_configured": "Device is already configured",
"no_ip_control": "IP Control is disabled on your TV or the TV is not supported.",
"not_bravia_device": "The device is not a Bravia TV.",
"reauth_successful": "Re-authentication was successful",
"reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again."
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
@@ -16,21 +15,27 @@
"step": {
"authorize": {
"data": {
"pin": "PIN Code",
"use_psk": "Use PSK authentication"
},
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN.",
"description": "Make sure that \u00abControl remotely\u00bb is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
"title": "Authorize Sony Bravia TV"
},
"confirm": {
"description": "Do you want to start setup?"
},
"reauth_confirm": {
"pin": {
"data": {
"pin": "PIN Code",
"use_psk": "Use PSK authentication"
"pin": "PIN Code"
},
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN."
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.",
"title": "Authorize Sony Bravia TV"
},
"psk": {
"data": {
"pin": "PSK"
},
"description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set \u00abAuthentication\u00bb to \u00abNormal and Pre-Shared Key\u00bb or \u00abPre-Shared Key\u00bb and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.",
"title": "Authorize Sony Bravia TV"
},
"user": {
"data": {

View File

@@ -17,7 +17,7 @@
"service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb"
}
],
"requirements": ["bthome-ble==2.4.0"],
"requirements": ["bthome-ble==2.4.1"],
"dependencies": ["bluetooth"],
"codeowners": ["@Ernst79"],
"iot_class": "local_push"

View File

@@ -1,7 +1,7 @@
"""Support for WebDav Calendar."""
from __future__ import annotations
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
import logging
import re
@@ -185,8 +185,8 @@ class WebDavCalendarData:
event_list.append(
CalendarEvent(
summary=self.get_attr_value(vevent, "summary") or "",
start=vevent.dtstart.value,
end=self.get_end_date(vevent),
start=self.to_local(vevent.dtstart.value),
end=self.to_local(self.get_end_date(vevent)),
location=self.get_attr_value(vevent, "location"),
description=self.get_attr_value(vevent, "description"),
)
@@ -269,8 +269,8 @@ class WebDavCalendarData:
)
self.event = CalendarEvent(
summary=summary,
start=vevent.dtstart.value,
end=self.get_end_date(vevent),
start=self.to_local(vevent.dtstart.value),
end=self.to_local(self.get_end_date(vevent)),
location=self.get_attr_value(vevent, "location"),
description=self.get_attr_value(vevent, "description"),
)
@@ -308,15 +308,23 @@ class WebDavCalendarData:
def to_datetime(obj):
"""Return a datetime."""
if isinstance(obj, datetime):
if obj.tzinfo is None:
# floating value, not bound to any time zone in particular
# represent same time regardless of which time zone is currently being observed
return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE)
return obj
return WebDavCalendarData.to_local(obj)
return dt.dt.datetime.combine(obj, dt.dt.time.min).replace(
tzinfo=dt.DEFAULT_TIME_ZONE
)
@staticmethod
def to_local(obj: datetime | date) -> datetime | date:
"""Return a datetime as a local datetime, leaving dates unchanged.
This handles giving floating times a timezone for comparison
with all day events and dropping the custom timezone object
used by the caldav client and dateutil so the datetime can be copied.
"""
if isinstance(obj, datetime):
return dt.as_local(obj)
return obj
@staticmethod
def get_attr_value(obj, attribute):
"""Return the value of the attribute if defined."""

View File

@@ -174,7 +174,10 @@ async def async_get_trigger_capabilities(
if trigger_type == "hvac_mode_changed":
return {
"extra_fields": vol.Schema(
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
{
vol.Required(state_trigger.CONF_TO): vol.In(const.HVAC_MODES),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
}

View File

@@ -10,6 +10,7 @@ from pycfdns.exceptions import (
CloudflareAuthenticationException,
CloudflareConnectionException,
CloudflareException,
CloudflareZoneException,
)
from homeassistant.config_entries import ConfigEntry
@@ -47,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
zone_id = await cfupdate.get_zone_id()
except CloudflareAuthenticationException as error:
raise ConfigEntryAuthFailed from error
except CloudflareConnectionException as error:
except (CloudflareConnectionException, CloudflareZoneException) as error:
raise ConfigEntryNotReady from error
async def update_records(now):

View File

@@ -75,7 +75,6 @@ async def async_setup(hass):
@websocket_api.websocket_command(
{
vol.Required("type"): "config/device_registry/update",
vol.Optional("aliases"): list,
vol.Optional("area_id"): vol.Any(str, None),
vol.Required("device_id"): str,
# We only allow setting disabled_by user via API.
@@ -96,10 +95,6 @@ def websocket_update_device(
msg.pop("type")
msg_id = msg.pop("id")
if "aliases" in msg:
# Convert aliases to a set
msg["aliases"] = set(msg["aliases"])
if msg.get("disabled_by") is not None:
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
@@ -165,7 +160,6 @@ async def websocket_remove_config_entry_from_device(
def _entry_dict(entry):
"""Convert entry to API format."""
return {
"aliases": entry.aliases,
"area_id": entry.area_id,
"configuration_url": entry.configuration_url,
"config_entries": list(entry.config_entries),

View File

@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==105"],
"requirements": ["pydeconz==106"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -21,7 +21,6 @@ from .devolo_device import DevoloDeviceEntity
DEVICE_CLASS_MAPPING = {
"battery": SensorDeviceClass.BATTERY,
"temperature": SensorDeviceClass.TEMPERATURE,
"light": SensorDeviceClass.ILLUMINANCE,
"humidity": SensorDeviceClass.HUMIDITY,
"current": SensorDeviceClass.POWER,
"total": SensorDeviceClass.ENERGY,

View File

@@ -28,6 +28,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
UnitOfEnergy,
UnitOfVolume,
)
from homeassistant.core import CoreState, Event, HomeAssistant, callback
@@ -401,7 +402,7 @@ async def async_setup_entry(
)
@Throttle(min_time_between_updates)
def update_entities_telegram(telegram: dict[str, DSMRObject]) -> None:
def update_entities_telegram(telegram: dict[str, DSMRObject] | None) -> None:
"""Update entities with latest telegram and trigger state update."""
# Make all device entities aware of new telegram
for entity in entities:
@@ -445,6 +446,11 @@ async def async_setup_entry(
while hass.state == CoreState.not_running or hass.is_running:
# Start DSMR asyncio.Protocol reader
# Reflect connected state in devices state by setting an
# empty telegram resulting in `unknown` states
update_entities_telegram({})
try:
transport, protocol = await hass.loop.create_task(reader_factory())
@@ -472,8 +478,8 @@ async def async_setup_entry(
protocol = None
# Reflect disconnect state in devices state by setting an
# empty telegram resulting in `unknown` states
update_entities_telegram({})
# None telegram resulting in `unavailable` states
update_entities_telegram(None)
# throttle reconnect attempts
await asyncio.sleep(
@@ -487,11 +493,19 @@ async def async_setup_entry(
transport = None
protocol = None
# Reflect disconnect state in devices state by setting an
# None telegram resulting in `unavailable` states
update_entities_telegram(None)
# throttle reconnect attempts
await asyncio.sleep(
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
)
except CancelledError:
# Reflect disconnect state in devices state by setting an
# None telegram resulting in `unavailable` states
update_entities_telegram(None)
if stop_listener and (
hass.state == CoreState.not_running or hass.is_running
):
@@ -534,7 +548,7 @@ class DSMREntity(SensorEntity):
"""Initialize entity."""
self.entity_description = entity_description
self._entry = entry
self.telegram: dict[str, DSMRObject] = {}
self.telegram: dict[str, DSMRObject] | None = {}
device_serial = entry.data[CONF_SERIAL_ID]
device_name = DEVICE_NAME_ELECTRICITY
@@ -551,16 +565,21 @@ class DSMREntity(SensorEntity):
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
@callback
def update_data(self, telegram: dict[str, DSMRObject]) -> None:
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
"""Update data."""
self.telegram = telegram
if self.hass and self.entity_description.obis_reference in self.telegram:
if self.hass and (
telegram is None or self.entity_description.obis_reference in telegram
):
self.async_write_ha_state()
def get_dsmr_object_attr(self, attribute: str) -> str | None:
"""Read attribute from last received telegram for this DSMR object."""
# Make sure telegram contains an object for this entities obis
if self.entity_description.obis_reference not in self.telegram:
if (
self.telegram is None
or self.entity_description.obis_reference not in self.telegram
):
return None
# Get the attribute value if the object has it
@@ -568,6 +587,26 @@ class DSMREntity(SensorEntity):
attr: str | None = getattr(dsmr_object, attribute)
return attr
@property
def available(self) -> bool:
"""Entity is only available if there is a telegram."""
return self.telegram is not None
@property
def device_class(self) -> SensorDeviceClass | None:
"""Return the device class of this entity."""
device_class = super().device_class
# Override device class for gas sensors providing energy units, like
# kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas
with suppress(ValueError):
if device_class == SensorDeviceClass.GAS and UnitOfEnergy(
str(self.native_unit_of_measurement)
):
return SensorDeviceClass.ENERGY
return device_class
@property
def native_value(self) -> StateType:
"""Return the state of sensor, if available, translate if needed."""

View File

@@ -560,8 +560,8 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
DSMRReaderSensorEntityDescription(
key="dsmr/consumption/quarter-hour-peak-electricity/average_delivered",
name="Previous quarter-hour peak usage",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
),
DSMRReaderSensorEntityDescription(
key="dsmr/consumption/quarter-hour-peak-electricity/read_at_start",

View File

@@ -3,7 +3,7 @@
"name": "Rheem EcoNet Products",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/econet",
"requirements": ["pyeconet==0.1.15"],
"requirements": ["pyeconet==0.1.18"],
"codeowners": ["@vangorra", "@w1ll1am23"],
"iot_class": "cloud_push",
"loggers": ["paho_mqtt", "pyeconet"]

View File

@@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = {
SensorStateClass.TOTAL_INCREASING,
}
VALID_ENERGY_UNITS: set[str] = {
UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.WATT_HOUR,
}
VALID_ENERGY_UNITS_GAS = {
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
*VALID_ENERGY_UNITS,
}
VALID_VOLUME_UNITS_WATER: set[str] = {
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.GALLONS,
UnitOfVolume.LITERS,

View File

@@ -22,10 +22,10 @@ from .const import DOMAIN
ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
ENERGY_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
)
}
ENERGY_PRICE_UNITS = tuple(
@@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = (
)
GAS_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.WATT_HOUR,
),
sensor.SensorDeviceClass.GAS: (
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
),
sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
}
GAS_PRICE_UNITS = tuple(
f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units
@@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price"
WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,)
WATER_USAGE_UNITS = {
sensor.SensorDeviceClass.WATER: (
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.GALLONS,
UnitOfVolume.LITERS,
),

View File

@@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription(
key="aqhi",
name="AQHI",
device_class=SensorDeviceClass.AQI,
native_unit_of_measurement="AQI",
state_class=SensorStateClass.MEASUREMENT,
value_fn=_get_aqhi_value,
)

View File

@@ -7,7 +7,8 @@
"zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"],
"after_dependencies": ["bluetooth", "zeroconf", "tag"],
"dependencies": ["bluetooth"],
"after_dependencies": ["zeroconf", "tag"],
"iot_class": "local_push",
"integration_type": "device",
"loggers": ["aioesphomeapi", "noiseprotocol"]

View File

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

View File

@@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None:
for target in call.data[ATTR_DEVICE_ID]:
device = registry.async_get(target)
if device:
coordinator = hass.data[DOMAIN][list(device.config_entries)[0]]
# fully_method(coordinator.fully, *args, **kwargs) would make
# test_services.py fail.
await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs)
for key in device.config_entries:
entry = hass.config_entries.async_get_entry(key)
if not entry:
continue
if entry.domain != DOMAIN:
continue
coordinator = hass.data[DOMAIN][key]
# fully_method(coordinator.fully, *args, **kwargs) would make
# test_services.py fail.
await getattr(coordinator.fully, fully_method.__name__)(
*args, **kwargs
)
break
async def async_load_url(call: ServiceCall) -> None:
"""Load a URL on the Fully Kiosk Browser."""

View File

@@ -221,8 +221,7 @@ async def async_setup_entry(
)
if (
search := data.get(CONF_SEARCH)
or calendar_item.access_role == AccessRole.FREE_BUSY_READER
):
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
coordinator = CalendarQueryUpdateCoordinator(
hass,
calendar_service,

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
"requirements": ["gcal-sync==4.1.0", "oauth2client==4.1.3"],
"requirements": ["gcal-sync==4.1.1", "oauth2client==4.1.3"],
"codeowners": ["@allenporter"],
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"]

View File

@@ -1,6 +1,8 @@
"""Helper classes for Google Assistant SDK integration."""
from __future__ import annotations
import logging
import aiohttp
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
@@ -12,6 +14,8 @@ from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES
_LOGGER = logging.getLogger(__name__)
DEFAULT_LANGUAGE_CODES = {
"de": "de-DE",
"en": "en-US",
@@ -39,7 +43,8 @@ async def async_send_text_commands(commands: list[str], hass: HomeAssistant) ->
language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
with TextAssistant(credentials, language_code) as assistant:
for command in commands:
assistant.assist(command)
text_response = assistant.assist(command)[0]
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
def default_language_code(hass: HomeAssistant):

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
"requirements": ["gassist-text==0.0.5"],
"requirements": ["gassist-text==0.0.7"],
"codeowners": ["@tronikos"],
"iot_class": "cloud_polling",
"integration_type": "service"

View File

@@ -22,7 +22,7 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialise growatt server flow."""
self.api = growattServer.GrowattApi()
self.api = None
self.user_id = None
self.data = {}
@@ -46,6 +46,10 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not user_input:
return self._async_show_user_form()
# Initialise the library with the username & a random id each time it is started
self.api = growattServer.GrowattApi(
add_random_user_id=True, agent_identifier=user_input[CONF_USERNAME]
)
self.api.server_url = user_input[CONF_URL]
login_response = await self.hass.async_add_executor_job(
self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]

View File

@@ -80,14 +80,8 @@ async def async_setup_entry(
config[CONF_URL] = url
hass.config_entries.async_update_entry(config_entry, data=config)
# Initialise the library with a random user id each time it is started,
# also extend the library's default identifier to include 'home-assistant'
api = growattServer.GrowattApi(
add_random_user_id=True,
agent_identifier=(
f"{growattServer.GrowattApi.agent_identifier} - home-assistant"
),
)
# Initialise the library with the username & a random id each time it is started
api = growattServer.GrowattApi(add_random_user_id=True, agent_identifier=username)
api.server_url = url
devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config)

View File

@@ -70,6 +70,7 @@ def api_error(
class AddonInfo:
"""Represent the current add-on info state."""
available: bool
hostname: str | None
options: dict[str, Any]
state: AddonState
@@ -144,6 +145,7 @@ class AddonManager:
self._logger.debug("Add-on store info: %s", addon_store_info)
if not addon_store_info["installed"]:
return AddonInfo(
available=addon_store_info["available"],
hostname=None,
options={},
state=AddonState.NOT_INSTALLED,
@@ -154,6 +156,7 @@ class AddonManager:
addon_info = await async_get_addon_info(self._hass, self.addon_slug)
addon_state = self.async_get_addon_state(addon_info)
return AddonInfo(
available=addon_info["available"],
hostname=addon_info["hostname"],
options=addon_info["options"],
state=addon_state,
@@ -184,6 +187,11 @@ class AddonManager:
@api_error("Failed to install the {addon_name} add-on")
async def async_install_addon(self) -> None:
"""Install the managed add-on."""
addon_info = await self.async_get_addon_info()
if not addon_info.available:
raise AddonError(f"{self.addon_name} add-on is not available anymore")
await async_install_addon(self._hass, self.addon_slug)
@api_error("Failed to uninstall the {addon_name} add-on")
@@ -196,6 +204,9 @@ class AddonManager:
"""Update the managed add-on if needed."""
addon_info = await self.async_get_addon_info()
if not addon_info.available:
raise AddonError(f"{self.addon_name} add-on is not available anymore")
if addon_info.state is AddonState.NOT_INSTALLED:
raise AddonError(f"{self.addon_name} add-on is not installed")

View File

@@ -5,7 +5,6 @@ from datetime import timedelta
from http import HTTPStatus
import logging
from aiohttp.hdrs import USER_AGENT
import requests
import voluptuous as vol
@@ -160,7 +159,7 @@ class HaveIBeenPwnedData:
"""Get the latest data for current email from REST service."""
try:
url = f"{URL}{self._email}?truncateResponse=false"
header = {USER_AGENT: HA_USER_AGENT, "hibp-api-key": self._api_key}
header = {"User-Agent": HA_USER_AGENT, "hibp-api-key": self._api_key}
_LOGGER.debug("Checking for breaches for email: %s", self._email)
req = requests.get(url, headers=header, allow_redirects=True, timeout=5)

View File

@@ -28,7 +28,7 @@ DATA_HYDRAWISE = "hydrawise"
DOMAIN = "hydrawise"
DEFAULT_WATERING_TIME = 15
SCAN_INTERVAL = timedelta(seconds=30)
SCAN_INTERVAL = timedelta(seconds=120)
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"

View File

@@ -1,7 +1,8 @@
"""DataUpdateCoordinator for LaCrosse View."""
from __future__ import annotations
from datetime import datetime, timedelta
from datetime import timedelta
from time import time
from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor
@@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
) -> None:
"""Initialize DataUpdateCoordinator for LaCrosse View."""
self.api = api
self.last_update = datetime.utcnow()
self.last_update = time()
self.username = entry.data["username"]
self.password = entry.data["password"]
self.hass = hass
@@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
async def _async_update_data(self) -> list[Sensor]:
"""Get the data for LaCrosse View."""
now = datetime.utcnow()
now = int(time())
if self.last_update < now - timedelta(minutes=59): # Get new token
if self.last_update < now - 59 * 60: # Get new token once in a hour
self.last_update = now
try:
await self.api.login(self.username, self.password)
except LoginError as error:
raise ConfigEntryAuthFailed from error
# Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request)
yesterday = now - timedelta(days=1)
yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0)
yesterday_timestamp = datetime.timestamp(yesterday)
try:
# Fetch last hour of data
sensors = await self.api.get_sensors(
location=Location(id=self.id, name=self.name),
tz=self.hass.config.time_zone,
start=str(int(yesterday_timestamp)),
end=str(int(datetime.timestamp(now))),
start=str(now - 3600),
end=str(now),
)
except HTTPError as error:
raise ConfigEntryNotReady from error

View File

@@ -3,7 +3,7 @@
"name": "LCN",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/lcn",
"requirements": ["pypck==0.7.15"],
"requirements": ["pypck==0.7.16"],
"codeowners": ["@alengwenus"],
"iot_class": "local_push",
"loggers": ["pypck"]

View File

@@ -194,7 +194,7 @@ class VarAbs(LcnServiceCall):
vol.Required(CONF_VARIABLE): vol.All(
vol.Upper, vol.In(VARIABLES + SETPOINTS)
),
vol.Optional(CONF_VALUE, default=0): cv.positive_int,
vol.Optional(CONF_VALUE, default=0): vol.Coerce(float),
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
vol.Upper, vol.In(VAR_UNITS)
),
@@ -234,7 +234,7 @@ class VarRel(LcnServiceCall):
vol.Required(CONF_VARIABLE): vol.All(
vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS)
),
vol.Optional(CONF_VALUE, default=0): int,
vol.Optional(CONF_VALUE, default=0): vol.Coerce(float),
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
vol.Upper, vol.In(VAR_UNITS)
),

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/life360",
"codeowners": ["@pnbruckner"],
"requirements": ["life360==5.3.0"],
"requirements": ["life360==5.5.0"],
"iot_class": "cloud_polling",
"loggers": ["life360"]
}

View File

@@ -3,7 +3,7 @@
"name": "Local Calendar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"requirements": ["ical==4.2.8"],
"requirements": ["ical==4.2.9"],
"codeowners": ["@allenporter"],
"iot_class": "local_polling",
"loggers": ["ical"]

View File

@@ -352,6 +352,13 @@ class MotionTiltDevice(MotionPositionDevice):
return None
return self._blind.angle * 100 / 180
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
if self._blind.position is None:
return None
return self._blind.position >= 95
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
async with self._api_lock:

View File

@@ -3,7 +3,7 @@
"name": "Motion Blinds",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"requirements": ["motionblinds==0.6.13"],
"requirements": ["motionblinds==0.6.15"],
"dependencies": ["network"],
"dhcp": [
{ "registered_devices": true },

View File

@@ -59,7 +59,7 @@ CONF_EXPIRE_AFTER = "expire_after"
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,

View File

@@ -6,7 +6,7 @@ import functools
import voluptuous as vol
from homeassistant.components import button
from homeassistant.components.button import ButtonEntity
from homeassistant.components.button import DEVICE_CLASSES_SCHEMA, ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
from homeassistant.core import HomeAssistant
@@ -39,7 +39,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,

View File

@@ -161,7 +161,7 @@ def validate_options(config: ConfigType) -> ConfigType:
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,

View File

@@ -87,7 +87,7 @@ def validate_config(config: ConfigType) -> ConfigType:
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
{
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float),
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float),
vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode),

View File

@@ -115,6 +115,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
discovery_data: DiscoveryInfoType | None,
) -> None:
"""Initialize the MQTT select."""
self._attr_current_option = None
SelectEntity.__init__(self)
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@@ -125,7 +126,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
self._attr_current_option = None
self._optimistic = config[CONF_OPTIMISTIC]
self._attr_options = config[CONF_OPTIONS]

View File

@@ -98,13 +98,13 @@ def validate_options(conf: ConfigType) -> ConfigType:
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_LAST_RESET_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)

View File

@@ -62,7 +62,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
vol.Optional(CONF_STATE_OFF): cv.string,
vol.Optional(CONF_STATE_ON): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)

View File

@@ -54,7 +54,7 @@ CONF_TITLE = "title"
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_ENTITY_PICTURE): cv.string,
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,

View File

@@ -3,7 +3,7 @@
"name": "Netatmo",
"integration_type": "hub",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": ["pyatmo==7.4.0"],
"requirements": ["pyatmo==7.5.0"],
"after_dependencies": ["cloud", "media_source"],
"dependencies": ["application_credentials", "webhook"],
"codeowners": ["@cgtobi"],

View File

@@ -2,7 +2,7 @@
"domain": "nissan_leaf",
"name": "Nissan Leaf",
"documentation": "https://www.home-assistant.io/integrations/nissan_leaf",
"requirements": ["pycarwings2==2.13"],
"requirements": ["pycarwings2==2.14"],
"codeowners": ["@filcole"],
"iot_class": "cloud_polling",
"loggers": ["pycarwings2"]

View File

@@ -91,7 +91,7 @@ class NumberDeviceClass(StrEnum):
CURRENT = "current"
"""Current.
Unit of measurement: `A`
Unit of measurement: `A`, `mA`
"""
DATA_RATE = "data_rate"
@@ -213,7 +213,7 @@ class NumberDeviceClass(StrEnum):
POWER_FACTOR = "power_factor"
"""Power factor.
Unit of measurement: `%`
Unit of measurement: `%`, `None`
"""
POWER = "power"
@@ -296,7 +296,7 @@ class NumberDeviceClass(StrEnum):
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`
Unit of measurement: `V`, `mV`
"""
VOLUME = "volume"

View File

@@ -210,7 +210,7 @@ class PhilipsTVMediaPlayer(
async def async_media_play_pause(self) -> None:
"""Send pause command to media player."""
if self._tv.quirk_playpause_spacebar:
await self._tv.sendUnicode(" ")
await self._tv.sendKey("Confirm")
else:
await self._tv.sendKey("PlayPause")
await self._async_update_soon()
@@ -509,6 +509,8 @@ class PhilipsTVMediaPlayer(
self._media_title = self._sources.get(self._tv.source_id)
self._media_channel = None
self._attr_assumed_state = True
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""

View File

@@ -6,12 +6,10 @@ from aiopurpleair.models.sensors import SensorModel
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .config_flow import async_remove_sensor_by_device_id
from .const import CONF_LAST_UPDATE_SENSOR_ADD, DOMAIN
from .const import DOMAIN
from .coordinator import PurpleAirDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -32,26 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_handle_entry_update(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle an options update."""
if entry.options.get(CONF_LAST_UPDATE_SENSOR_ADD) is True:
# If the last options update was to add a sensor, we reload the config entry:
await hass.config_entries.async_reload(entry.entry_id)
async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
) -> bool:
"""Remove a config entry from a device."""
new_entry_options = async_remove_sensor_by_device_id(
hass,
config_entry,
device_entry.id,
# remove_device is set to False because in this instance, the device has
# already been removed:
remove_device=False,
)
return hass.config_entries.async_update_entry(
config_entry, options=new_entry_options
)
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -1,6 +1,7 @@
"""Config flow for PurpleAir integration."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from copy import deepcopy
from dataclasses import dataclass, field
@@ -14,13 +15,15 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
@@ -28,7 +31,7 @@ from homeassistant.helpers.selector import (
SelectSelectorMode,
)
from .const import CONF_LAST_UPDATE_SENSOR_ADD, CONF_SENSOR_INDICES, DOMAIN, LOGGER
from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER
CONF_DISTANCE = "distance"
CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options"
@@ -74,8 +77,7 @@ def async_get_nearby_sensors_options(
"""Return a set of nearby sensors as SelectOptionDict objects."""
return [
SelectOptionDict(
value=str(result.sensor.sensor_index),
label=f"{result.sensor.name} ({round(result.distance, 1)} km away)",
value=str(result.sensor.sensor_index), label=cast(str, result.sensor.name)
)
for result in nearby_sensor_results
]
@@ -118,50 +120,6 @@ def async_get_remove_sensor_schema(sensors: list[SelectOptionDict]) -> vol.Schem
)
@callback
def async_get_sensor_index(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
) -> int:
"""Get the sensor index related to a config and device entry.
Note that this method expects that there will always be a single sensor index per
DeviceEntry.
"""
[sensor_index] = [
sensor_index
for sensor_index in config_entry.options[CONF_SENSOR_INDICES]
if (DOMAIN, str(sensor_index)) in device_entry.identifiers
]
return cast(int, sensor_index)
@callback
def async_remove_sensor_by_device_id(
hass: HomeAssistant,
config_entry: ConfigEntry,
device_id: str,
*,
remove_device: bool = True,
) -> dict[str, Any]:
"""Remove a sensor and return update config entry options."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device_id)
assert device_entry
removed_sensor_index = async_get_sensor_index(hass, config_entry, device_entry)
options = deepcopy({**config_entry.options})
options[CONF_LAST_UPDATE_SENSOR_ADD] = False
options[CONF_SENSOR_INDICES].remove(removed_sensor_index)
if remove_device:
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry.entry_id
)
return options
@dataclass
class ValidationResult:
"""Define a validation result."""
@@ -408,7 +366,6 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow):
return self.async_abort(reason="already_configured")
options = deepcopy({**self.config_entry.options})
options[CONF_LAST_UPDATE_SENSOR_ADD] = True
options[CONF_SENSOR_INDICES].append(sensor_index)
return self.async_create_entry(title="", data=options)
@@ -433,8 +390,50 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow):
),
)
new_entry_options = async_remove_sensor_by_device_id(
self.hass, self.config_entry, user_input[CONF_SENSOR_DEVICE_ID]
device_registry = dr.async_get(self.hass)
entity_registry = er.async_get(self.hass)
device_id = user_input[CONF_SENSOR_DEVICE_ID]
device_entry = cast(dr.DeviceEntry, device_registry.async_get(device_id))
# Determine the entity entries that belong to this device.
entity_entries = er.async_entries_for_device(
entity_registry, device_id, include_disabled_entities=True
)
return self.async_create_entry(title="", data=new_entry_options)
device_entities_removed_event = asyncio.Event()
@callback
def async_device_entity_state_changed(_: Event) -> None:
"""Listen and respond when all device entities are removed."""
if all(
self.hass.states.get(entity_entry.entity_id) is None
for entity_entry in entity_entries
):
device_entities_removed_event.set()
# Track state changes for this device's entities and when they're removed,
# finish the flow:
cancel_state_track = async_track_state_change_event(
self.hass,
[entity_entry.entity_id for entity_entry in entity_entries],
async_device_entity_state_changed,
)
device_registry.async_update_device(
device_id, remove_config_entry_id=self.config_entry.entry_id
)
await device_entities_removed_event.wait()
# Once we're done, we can cancel the state change tracker callback:
cancel_state_track()
# Build new config entry options:
removed_sensor_index = next(
sensor_index
for sensor_index in self.config_entry.options[CONF_SENSOR_INDICES]
if (DOMAIN, str(sensor_index)) in device_entry.identifiers
)
options = deepcopy({**self.config_entry.options})
options[CONF_SENSOR_INDICES].remove(removed_sensor_index)
return self.async_create_entry(title="", data=options)

View File

@@ -5,6 +5,5 @@ DOMAIN = "purpleair"
LOGGER = logging.getLogger(__package__)
CONF_LAST_UPDATE_SENSOR_ADD = "last_update_sensor_add"
CONF_READ_KEY = "read_key"
CONF_SENSOR_INDICES = "sensor_indices"

View File

@@ -166,7 +166,7 @@ SENSOR_DESCRIPTIONS = [
name="Uptime",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:timer",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda sensor: sensor.uptime,
@@ -174,8 +174,7 @@ SENSOR_DESCRIPTIONS = [
PurpleAirSensorEntityDescription(
key="voc",
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_IAQ,
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda sensor: sensor.voc,
),

View File

@@ -9,19 +9,22 @@ import logging
from aiohttp import ClientConnectorError
import async_timeout
from reolink_ip.exceptions import ApiError, InvalidContentTypeError
from reolink_aio.exceptions import ApiError, InvalidContentTypeError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DEVICE_UPDATE_INTERVAL, DOMAIN, PLATFORMS
from .const import DOMAIN
from .host import ReolinkHost
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.CAMERA]
DEVICE_UPDATE_INTERVAL = 60
@dataclass
class ReolinkData:
@@ -31,14 +34,15 @@ class ReolinkData:
device_coordinator: DataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Reolink from a config entry."""
host = ReolinkHost(hass, dict(entry.data), dict(entry.options))
host = ReolinkHost(hass, config_entry.data, config_entry.options)
try:
if not await host.async_init():
raise ConfigEntryNotReady(
f"Error while trying to setup {host.api.host}:{host.api.port}: failed to obtain data from device."
f"Error while trying to setup {host.api.host}:{host.api.port}: "
"failed to obtain data from device."
)
except (
ClientConnectorError,
@@ -50,14 +54,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".'
) from err
entry.async_on_unload(
config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop)
)
async def async_device_config_update():
"""Perform the update of the host config-state cache, and renew the ONVIF-subscription."""
"""Update the host state cache and renew the ONVIF-subscription."""
async with async_timeout.timeout(host.api.timeout):
await host.update_states() # Login session is implicitly updated here, so no need to explicitly do it in a timer
# Login session is implicitly updated here
await host.update_states()
coordinator_device_config_update = DataUpdateCoordinator(
hass,
@@ -69,30 +74,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Fetch initial data so we have data when entities subscribe
await coordinator_device_config_update.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ReolinkData(
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
host=host,
device_coordinator=coordinator_device_config_update,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(entry_update_listener))
config_entry.async_on_unload(
config_entry.add_update_listener(entry_update_listener)
)
return True
async def entry_update_listener(hass: HomeAssistant, entry: ConfigEntry):
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
"""Update the configuration of the host entity."""
await hass.config_entries.async_reload(entry.entry_id)
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
host: ReolinkHost = hass.data[DOMAIN][entry.entry_id].host
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
await host.stop()
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok

View File

@@ -8,9 +8,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ReolinkData
from .const import DOMAIN
from .entity import ReolinkCoordinatorEntity
from .host import ReolinkHost
_LOGGER = logging.getLogger(__name__)
@@ -18,10 +18,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_devices: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a Reolink IP Camera."""
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
host = reolink_data.host
cameras = []
for channel in host.api.channels:
@@ -30,25 +31,31 @@ async def async_setup_entry(
streams.append("ext")
for stream in streams:
cameras.append(ReolinkCamera(hass, config_entry, channel, stream))
cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream))
async_add_devices(cameras, update_before_add=True)
async_add_entities(cameras, update_before_add=True)
class ReolinkCamera(ReolinkCoordinatorEntity, Camera):
"""An implementation of a Reolink IP camera."""
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
_attr_has_entity_name = True
def __init__(self, hass, config, channel, stream):
def __init__(
self,
reolink_data: ReolinkData,
config_entry: ConfigEntry,
channel: int,
stream: str,
) -> None:
"""Initialize Reolink camera stream."""
ReolinkCoordinatorEntity.__init__(self, hass, config)
ReolinkCoordinatorEntity.__init__(self, reolink_data, config_entry, channel)
Camera.__init__(self)
self._channel = channel
self._stream = stream
self._attr_name = f"{self._host.api.camera_name(self._channel)} {self._stream}"
self._attr_name = self._stream
self._attr_unique_id = f"{self._host.unique_id}_{self._channel}_{self._stream}"
self._attr_entity_registry_enabled_default = stream == "sub"

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
import logging
from typing import cast
from typing import Any
from reolink_ip.exceptions import ApiError, CredentialsInvalidError
from reolink_aio.exceptions import ApiError, CredentialsInvalidError
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
@@ -18,6 +18,8 @@ from .host import ReolinkHost
_LOGGER = logging.getLogger(__name__)
DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL}
class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Reolink options."""
@@ -26,10 +28,12 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize ReolinkOptionsFlowHandler."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None) -> FlowResult:
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the Reolink options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_create_entry(data=user_input)
return self.async_show_form(
step_id="init",
@@ -37,9 +41,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
{
vol.Required(
CONF_PROTOCOL,
default=self.config_entry.options.get(
CONF_PROTOCOL, DEFAULT_PROTOCOL
),
default=self.config_entry.options[CONF_PROTOCOL],
): vol.In(["rtsp", "rtmp"]),
}
),
@@ -51,8 +53,6 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
host: ReolinkHost | None = None
@staticmethod
@callback
def async_get_options_flow(
@@ -61,14 +61,16 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Options callback for Reolink."""
return ReolinkOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None) -> FlowResult:
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
placeholders = {}
if user_input is not None:
try:
await self.async_obtain_host_settings(self.hass, user_input)
host = await async_obtain_host_settings(self.hass, user_input)
except CannotConnect:
errors[CONF_HOST] = "cannot_connect"
except CredentialsInvalidError:
@@ -81,19 +83,17 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
placeholders["error"] = str(err)
errors[CONF_HOST] = "unknown"
self.host = cast(ReolinkHost, self.host)
if not errors:
user_input[CONF_PORT] = self.host.api.port
user_input[CONF_USE_HTTPS] = self.host.api.use_https
user_input[CONF_PORT] = host.api.port
user_input[CONF_USE_HTTPS] = host.api.use_https
await self.async_set_unique_id(
self.host.unique_id, raise_on_progress=False
)
await self.async_set_unique_id(host.unique_id, raise_on_progress=False)
self._abort_if_unique_id_configured(updates=user_input)
return self.async_create_entry(
title=str(self.host.api.nvr_name), data=user_input
title=str(host.api.nvr_name),
data=user_input,
options=DEFAULT_OPTIONS,
)
data_schema = vol.Schema(
@@ -118,19 +118,20 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders=placeholders,
)
async def async_obtain_host_settings(
self, hass: core.HomeAssistant, user_input: dict
):
"""Initialize the Reolink host and get the host information."""
host = ReolinkHost(hass, user_input, {})
try:
if not await host.async_init():
raise CannotConnect
finally:
await host.stop()
async def async_obtain_host_settings(
hass: core.HomeAssistant, user_input: dict
) -> ReolinkHost:
"""Initialize the Reolink host and get the host information."""
host = ReolinkHost(hass, user_input, DEFAULT_OPTIONS)
self.host = host
try:
if not await host.async_init():
raise CannotConnect
finally:
await host.stop()
return host
class CannotConnect(exceptions.HomeAssistantError):

View File

@@ -1,13 +1,9 @@
"""Constants for the Reolink Camera integration."""
DOMAIN = "reolink"
PLATFORMS = ["camera"]
CONF_USE_HTTPS = "use_https"
CONF_PROTOCOL = "protocol"
DEFAULT_PROTOCOL = "rtsp"
DEFAULT_TIMEOUT = 60
HOST = "host"
DEVICE_UPDATE_INTERVAL = 60

View File

@@ -1,5 +1,7 @@
"""Reolink parent entity class."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -11,24 +13,20 @@ from .const import DOMAIN
class ReolinkCoordinatorEntity(CoordinatorEntity):
"""Parent class for Reolink Entities."""
def __init__(self, hass, config):
def __init__(
self, reolink_data: ReolinkData, config_entry: ConfigEntry, channel: int | None
) -> None:
"""Initialize ReolinkCoordinatorEntity."""
self._hass = hass
entry_data: ReolinkData = self._hass.data[DOMAIN][config.entry_id]
coordinator = entry_data.device_coordinator
coordinator = reolink_data.device_coordinator
super().__init__(coordinator)
self._host = entry_data.host
self._channel = None
self._host = reolink_data.host
self._channel = channel
@property
def device_info(self):
"""Information about this entity/device."""
http_s = "https" if self._host.api.use_https else "http"
conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
if self._host.api.is_nvr and self._channel is not None:
return DeviceInfo(
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")},
via_device=(DOMAIN, self._host.unique_id),
name=self._host.api.camera_name(self._channel),
@@ -36,19 +34,19 @@ class ReolinkCoordinatorEntity(CoordinatorEntity):
manufacturer=self._host.api.manufacturer,
configuration_url=conf_url,
)
return DeviceInfo(
identifiers={(DOMAIN, self._host.unique_id)},
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
name=self._host.api.nvr_name,
model=self._host.api.model,
manufacturer=self._host.api.manufacturer,
hw_version=self._host.api.hardware_version,
sw_version=self._host.api.sw_version,
configuration_url=conf_url,
)
else:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._host.unique_id)},
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
name=self._host.api.nvr_name,
model=self._host.api.model,
manufacturer=self._host.api.manufacturer,
hw_version=self._host.api.hardware_version,
sw_version=self._host.api.sw_version,
configuration_url=conf_url,
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._host.api.session_active
return self._host.api.session_active and super().available

View File

@@ -2,11 +2,13 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
import aiohttp
from reolink_ip.api import Host
from reolink_ip.exceptions import (
from reolink_aio.api import Host
from reolink_aio.exceptions import (
ApiError,
CredentialsInvalidError,
InvalidContentTypeError,
@@ -16,7 +18,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DEFAULT_TIMEOUT
from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -27,18 +29,14 @@ class ReolinkHost:
def __init__(
self,
hass: HomeAssistant,
config: dict,
options: dict,
config: Mapping[str, Any],
options: Mapping[str, Any],
) -> None:
"""Initialize Reolink Host. Could be either NVR, or Camera."""
self._hass: HomeAssistant = hass
self._clientsession: aiohttp.ClientSession | None = None
self._unique_id: str | None = None
cur_protocol = (
DEFAULT_PROTOCOL if CONF_PROTOCOL not in options else options[CONF_PROTOCOL]
)
self._unique_id: str = ""
self._api = Host(
config[CONF_HOST],
@@ -46,12 +44,12 @@ class ReolinkHost:
config[CONF_PASSWORD],
port=config.get(CONF_PORT),
use_https=config.get(CONF_USE_HTTPS),
protocol=cur_protocol,
protocol=options[CONF_PROTOCOL],
timeout=DEFAULT_TIMEOUT,
)
@property
def unique_id(self):
def unique_id(self) -> str:
"""Create the unique ID, base for all entities."""
return self._unique_id
@@ -99,23 +97,22 @@ class ReolinkHost:
):
if enable_onvif:
_LOGGER.error(
"Unable to switch on ONVIF on %s. You need it to be ON to receive notifications",
"Failed to enable ONVIF on %s. Set it to ON to receive notifications",
self._api.nvr_name,
)
if enable_rtmp:
_LOGGER.error(
"Unable to switch on RTMP on %s. You need it to be ON",
"Failed to enable RTMP on %s. Set it to ON",
self._api.nvr_name,
)
elif enable_rtsp:
_LOGGER.error(
"Unable to switch on RTSP on %s. You need it to be ON",
"Failed to enable RTSP on %s. Set it to ON",
self._api.nvr_name,
)
if self._unique_id is None:
self._unique_id = format_mac(self._api.mac_address)
self._unique_id = format_mac(self._api.mac_address)
return True

View File

@@ -3,10 +3,8 @@
"name": "Reolink IP NVR/camera",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/reolink",
"requirements": ["reolink-ip==0.0.40"],
"dependencies": ["webhook"],
"after_dependencies": ["http"],
"codeowners": ["@starkillerOG", "@JimStar"],
"requirements": ["reolink-aio==0.1.3"],
"codeowners": ["@starkillerOG"],
"iot_class": "local_polling",
"loggers": ["reolink-ip"]
"loggers": ["reolink-aio"]
}

View File

@@ -3,7 +3,7 @@
"name": "RoonLabs music player",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roon",
"requirements": ["roonapi==0.1.1"],
"requirements": ["roonapi==0.1.2"],
"codeowners": ["@pavoni"],
"iot_class": "local_push",
"loggers": ["roonapi"]

View File

@@ -86,7 +86,11 @@ from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DataRateConverter,
DistanceConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
InformationConverter,
MassConverter,
PressureConverter,
SpeedConverter,
@@ -183,7 +187,7 @@ class SensorDeviceClass(StrEnum):
CURRENT = "current"
"""Current.
Unit of measurement: `A`
Unit of measurement: `A`, `mA`
"""
DATA_RATE = "data_rate"
@@ -305,7 +309,7 @@ class SensorDeviceClass(StrEnum):
POWER_FACTOR = "power_factor"
"""Power factor.
Unit of measurement: `%`
Unit of measurement: `%`, `None`
"""
POWER = "power"
@@ -388,7 +392,7 @@ class SensorDeviceClass(StrEnum):
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`
Unit of measurement: `V`, `mV`
"""
VOLUME = "volume"
@@ -466,12 +470,16 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in
# `entity-registry-settings.ts`
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
SensorDeviceClass.DATA_RATE: DataRateConverter,
SensorDeviceClass.DATA_SIZE: InformationConverter,
SensorDeviceClass.DISTANCE: DistanceConverter,
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
SensorDeviceClass.GAS: VolumeConverter,
SensorDeviceClass.PRECIPITATION: DistanceConverter,
SensorDeviceClass.PRESSURE: PressureConverter,
SensorDeviceClass.SPEED: SpeedConverter,
SensorDeviceClass.TEMPERATURE: TemperatureConverter,
SensorDeviceClass.VOLTAGE: ElectricPotentialConverter,
SensorDeviceClass.VOLUME: VolumeConverter,
SensorDeviceClass.WATER: VolumeConverter,
SensorDeviceClass.WEIGHT: MassConverter,
@@ -485,7 +493,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.BATTERY: {PERCENTAGE},
SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION},
SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
SensorDeviceClass.CURRENT: {UnitOfElectricCurrent.AMPERE},
SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent),
SensorDeviceClass.DATA_RATE: set(UnitOfDataRate),
SensorDeviceClass.DATA_SIZE: set(UnitOfInformation),
SensorDeviceClass.DISTANCE: set(UnitOfLength),
@@ -513,7 +521,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE},
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT},
SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
@@ -533,7 +541,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
},
SensorDeviceClass.VOLTAGE: {UnitOfElectricPotential.VOLT},
SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential),
SensorDeviceClass.VOLUME: set(UnitOfVolume),
SensorDeviceClass.WATER: {
UnitOfVolume.CENTUM_CUBIC_FEET,
@@ -960,6 +968,7 @@ class SensorEntity(Entity):
# Validate unit of measurement used for sensors with a device class
if (
not self._invalid_unit_of_measurement_reported
and value is not None
and device_class
and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None
and native_unit_of_measurement not in units

View File

@@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [
selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"),
]
INTERNAL_WIFI_AP_IP = "192.168.33.1"
async def validate_input(
hass: HomeAssistant,
@@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
current_entry := await self.async_set_unique_id(mac)
) and current_entry.data[CONF_HOST] == host:
await async_reconnect_soon(self.hass, current_entry)
self._abort_if_unique_id_configured({CONF_HOST: host})
if host == INTERNAL_WIFI_AP_IP:
# If the device is broadcasting the internal wifi ap ip
# we can't connect to it, so we should not update the
# entry with the new host as it will be unreachable
#
# This is a workaround for a bug in the firmware 0.12 (and older?)
# which should be removed once the firmware is fixed
# and the old version is no longer in use
self._abort_if_unique_id_configured()
else:
self._abort_if_unique_id_configured({CONF_HOST: host})
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo

View File

@@ -706,11 +706,12 @@ async def _async_find_next_available_port(source: AddressTupleVXType) -> int:
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
for port in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT):
addr = (source[0],) + (port,) + source[2:]
try:
test_socket.bind(source)
test_socket.bind(addr)
return port
except OSError:
if port == UPNP_SERVER_MAX_PORT:
if port == UPNP_SERVER_MAX_PORT - 1:
raise
raise RuntimeError("unreachable")

View File

@@ -45,7 +45,11 @@ PLATFORMS_BY_TYPE = {
SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.HUMIDIFIER.value: [Platform.HUMIDIFIER, Platform.SENSOR],
SupportedModels.LOCK.value: [Platform.BINARY_SENSOR, Platform.LOCK],
SupportedModels.LOCK.value: [
Platform.BINARY_SENSOR,
Platform.LOCK,
Platform.SENSOR,
],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,

View File

@@ -5,7 +5,9 @@ import logging
from typing import Any
from switchbot import (
SwitchbotAccountConnectionError,
SwitchBotAdvertisement,
SwitchbotAuthenticationError,
SwitchbotLock,
SwitchbotModel,
parse_advertisement_data,
@@ -17,7 +19,12 @@ from homeassistant.components.bluetooth import (
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE
from homeassistant.const import (
CONF_ADDRESS,
CONF_PASSWORD,
CONF_SENSOR_TYPE,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
@@ -94,6 +101,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
"name": data["modelFriendlyName"],
"address": short_address(discovery_info.address),
}
if model_name == SwitchbotModel.LOCK:
return await self.async_step_lock_choose_method()
if self._discovered_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self.async_step_confirm()
@@ -151,6 +160,62 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
async def async_step_lock_auth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the SwitchBot API auth step."""
errors = {}
assert self._discovered_adv is not None
description_placeholders = {}
if user_input is not None:
try:
key_details = await self.hass.async_add_executor_job(
SwitchbotLock.retrieve_encryption_key,
self._discovered_adv.address,
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
except SwitchbotAccountConnectionError as ex:
raise AbortFlow("cannot_connect") from ex
except SwitchbotAuthenticationError as ex:
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
errors = {"base": "auth_failed"}
description_placeholders = {"error_detail": str(ex)}
else:
return await self.async_step_lock_key(key_details)
user_input = user_input or {}
return self.async_show_form(
step_id="lock_auth",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
): str,
vol.Required(CONF_PASSWORD): str,
}
),
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
**description_placeholders,
},
)
async def async_step_lock_choose_method(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the SwitchBot API chose method step."""
assert self._discovered_adv is not None
return self.async_show_menu(
step_id="lock_choose_method",
menu_options=["lock_auth", "lock_key"],
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
},
)
async def async_step_lock_key(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -160,12 +225,11 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
if not await SwitchbotLock.verify_encryption_key(
self._discovered_adv.device,
user_input.get(CONF_KEY_ID),
user_input.get(CONF_ENCRYPTION_KEY),
user_input[CONF_KEY_ID],
user_input[CONF_ENCRYPTION_KEY],
):
errors = {
CONF_KEY_ID: "key_id_invalid",
CONF_ENCRYPTION_KEY: "encryption_key_invalid",
"base": "encryption_key_invalid",
}
else:
return await self._async_create_entry_from_discovery(user_input)
@@ -229,7 +293,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
device_adv = self._discovered_advs[user_input[CONF_ADDRESS]]
await self._async_set_device(device_adv)
if device_adv.data.get("modelName") == SwitchbotModel.LOCK:
return await self.async_step_lock_key()
return await self.async_step_lock_choose_method()
if device_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self._async_create_entry_from_discovery(user_input)
@@ -241,7 +305,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
device_adv = list(self._discovered_advs.values())[0]
await self._async_set_device(device_adv)
if device_adv.data.get("modelName") == SwitchbotModel.LOCK:
return await self.async_step_lock_key()
return await self.async_step_lock_choose_method()
if device_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self.async_step_confirm()

View File

@@ -2,7 +2,7 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.33.0"],
"requirements": ["PySwitchbot==0.36.3"],
"config_flow": true,
"dependencies": ["bluetooth"],
"codeowners": [

View File

@@ -22,11 +22,25 @@
"key_id": "Key ID",
"encryption_key": "Encryption key"
}
},
"lock_auth": {
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"lock_choose_method": {
"description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
"menu_options": {
"lock_auth": "SwitchBot account (recommended)",
"lock_key": "Enter lock encryption key manually"
}
}
},
"error": {
"key_id_invalid": "Key ID or Encryption key is invalid",
"encryption_key_invalid": "Key ID or Encryption key is invalid"
"encryption_key_invalid": "Key ID or Encryption key is invalid",
"auth_failed": "Authentication failed: {error_detail}"
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@@ -7,11 +7,36 @@
"switchbot_unsupported_type": "Unsupported Switchbot Type.",
"unknown": "Unexpected error"
},
"error": {
"auth_failed": "Authentication failed: {error_detail}",
"encryption_key_invalid": "Key ID or Encryption key is invalid"
},
"flow_title": "{name} ({address})",
"step": {
"confirm": {
"description": "Do you want to set up {name}?"
},
"lock_auth": {
"data": {
"password": "Password",
"username": "Username"
},
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive."
},
"lock_choose_method": {
"description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
"menu_options": {
"lock_auth": "SwitchBot account (recommended)",
"lock_key": "Enter lock encryption key manually"
}
},
"lock_key": {
"data": {
"encryption_key": "Encryption key",
"key_id": "Key ID"
},
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation."
},
"password": {
"data": {
"password": "Password"
@@ -22,18 +47,7 @@
"data": {
"address": "Device address"
}
},
"lock_key": {
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.",
"data": {
"key_id": "Key ID",
"encryption_key": "Encryption key"
}
}
},
"error": {
"key_id_invalid": "Key ID or Encryption key is invalid",
"encryption_key_invalid": "Key ID or Encryption key is invalid"
}
},
"options": {
@@ -45,4 +59,4 @@
}
}
}
}
}

View File

@@ -3,7 +3,7 @@
"name": "Tasmota",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tasmota",
"requirements": ["hatasmota==0.6.1"],
"requirements": ["hatasmota==0.6.2"],
"dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"],

View File

@@ -21,6 +21,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
POWER_VOLT_AMPERE_REACTIVE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
@@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = {
hc.LIGHT_LUX: LIGHT_LUX,
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
hc.PERCENTAGE: PERCENTAGE,
hc.POWER_FACTOR: None,
hc.POWER_WATT: UnitOfPower.WATT,
hc.PRESSURE_HPA: UnitOfPressure.HPA,
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR,

View File

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

View File

@@ -65,6 +65,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
self.entity_description = description
self._removed = False
self._write_state = False
self._attr_available = description.available_fn(controller, obj_id)
self._attr_device_info = description.device_info_fn(controller.api, obj_id)
@@ -117,9 +118,14 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
self.hass.async_create_task(self.remove_item({self._obj_id}))
return
self._attr_available = description.available_fn(self.controller, self._obj_id)
if (
available := description.available_fn(self.controller, self._obj_id)
) != self.available:
self._attr_available = available
self._write_state = True
self.async_update_state(event, obj_id)
self.async_write_ha_state()
if self._write_state:
self.async_write_ha_state()
@callback
def async_signal_reachable_callback(self) -> None:

View File

@@ -3,7 +3,7 @@
"name": "UniFi Network",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
"requirements": ["aiounifi==42"],
"requirements": ["aiounifi==43"],
"codeowners": ["@Kane610"],
"quality_scale": "platinum",
"ssdp": [

View File

@@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
self.async_on_remove(
handler.subscribe(
self.async_signalling_callback,
id_filter=self._obj_id,
)
)
self.async_on_remove(
@@ -253,11 +254,19 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
self.hass.async_create_task(self.remove_item({self._obj_id}))
return
update_state = False
obj = description.object_fn(self.controller.api, self._obj_id)
if (value := description.value_fn(self.controller, obj)) != self.native_value:
self._attr_native_value = value
self._attr_available = description.available_fn(self.controller, self._obj_id)
self.async_write_ha_state()
update_state = True
if (
available := description.available_fn(self.controller, self._obj_id)
) != self.available:
self._attr_available = available
update_state = True
if update_state:
self.async_write_ha_state()
@callback
def async_signal_reachable_callback(self) -> None:

View File

@@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
self.async_on_remove(
handler.subscribe(
self.async_signalling_callback,
id_filter=self._obj_id,
)
)
self.async_on_remove(
@@ -410,11 +411,20 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
self.hass.async_create_task(self.remove_item({self._obj_id}))
return
update_state = False
if not description.only_event_for_state_change:
obj = description.object_fn(self.controller.api, self._obj_id)
self._attr_is_on = description.is_on_fn(self.controller.api, obj)
self._attr_available = description.available_fn(self.controller, self._obj_id)
self.async_write_ha_state()
if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on:
self._attr_is_on = is_on
update_state = True
if (
available := description.available_fn(self.controller, self._obj_id)
) != self.available:
self._attr_available = available
update_state = True
if update_state:
self.async_write_ha_state()
@callback
def async_signal_reachable_callback(self) -> None:

View File

@@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity):
description = self.entity_description
obj = description.object_fn(self.controller.api, self._obj_id)
self._attr_in_progress = description.state_fn(self.controller.api, obj)
if (
in_progress := description.state_fn(self.controller.api, obj)
) != self.in_progress:
self._attr_in_progress = in_progress
self._write_state = True
self._attr_installed_version = obj.version
self._attr_latest_version = obj.upgrade_to_firmware or obj.version
if self.installed_version != self.latest_version:
self._write_state = True

View File

@@ -2,7 +2,7 @@
"domain": "xmpp",
"name": "Jabber (XMPP)",
"documentation": "https://www.home-assistant.io/integrations/xmpp",
"requirements": ["slixmpp==1.8.2"],
"requirements": ["slixmpp==1.8.3"],
"codeowners": ["@fabaff", "@flowolf"],
"iot_class": "cloud_push",
"loggers": ["pyasn1", "slixmpp"]

View File

@@ -17,6 +17,7 @@ from zigpy.application import ControllerApplication
from zigpy.config import CONF_DEVICE
import zigpy.device
import zigpy.endpoint
import zigpy.exceptions
import zigpy.group
from zigpy.types.named import EUI64
@@ -24,6 +25,7 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH
from homeassistant.components.system_log import LogEntry, _figure_out_source
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo
@@ -172,6 +174,8 @@ class ZHAGateway:
self.application_controller = await app_controller_cls.new(
app_config, auto_form=True, start_radio=True
)
except zigpy.exceptions.TransientConnectionError as exc:
raise ConfigEntryNotReady from exc
except Exception as exc: # pylint: disable=broad-except
_LOGGER.warning(
"Couldn't start %s coordinator (attempt %s of %s)",

View File

@@ -4,12 +4,12 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows==0.34.5",
"bellows==0.34.6",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.89",
"zha-quirks==0.0.90",
"zigpy-deconz==0.19.2",
"zigpy==0.52.3",
"zigpy==0.53.0",
"zigpy-xbee==0.16.2",
"zigpy-zigate==0.10.3",
"zigpy-znp==0.9.2"

View File

@@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"):
"""RSSI sensor for a device."""
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
_attr_should_poll = True # BaseZhaEntity defaults to False

View File

@@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
PRESSURE_SENSORS,
SIGNAL_STRENGTH_SENSORS,
TEMPERATURE_SENSORS,
UNIT_A_WEIGHTED_DECIBELS,
UNIT_AMPERE as SENSOR_UNIT_AMPERE,
UNIT_BTU_H,
UNIT_CELSIUS,
@@ -52,6 +53,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
UNIT_INCHES_PER_HOUR,
UNIT_KILOGRAM,
UNIT_KILOHERTZ,
UNIT_KILOPASCAL,
UNIT_LITER,
UNIT_LUX,
UNIT_M_S,
@@ -69,6 +71,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
UNIT_RSSI,
UNIT_SECOND,
UNIT_SYSTOLIC,
UNIT_UV_INDEX,
UNIT_VOLT as SENSOR_UNIT_VOLT,
UNIT_WATT as SENSOR_UNIT_WATT,
UNIT_WATT_PER_SQUARE_METER,
@@ -94,8 +97,8 @@ from homeassistant.const import (
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UV_INDEX,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -105,6 +108,7 @@ from homeassistant.const import (
UnitOfMass,
UnitOfPower,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
@@ -134,7 +138,7 @@ from .const import (
)
from .helpers import ZwaveValueID
METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
METER_DEVICE_CLASS_MAP: dict[str, list[MeterScaleType]] = {
ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES,
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_METER_TYPES,
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: ENERGY_TOTAL_INCREASING_METER_TYPES,
@@ -142,7 +146,7 @@ METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
ENTITY_DESC_KEY_POWER_FACTOR: POWER_FACTOR_METER_TYPES,
}
MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = {
MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = {
ENTITY_DESC_KEY_CO: CO_SENSORS,
ENTITY_DESC_KEY_CO2: CO2_SENSORS,
ENTITY_DESC_KEY_CURRENT: CURRENT_SENSORS,
@@ -156,7 +160,7 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = {
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS,
}
METER_UNIT_MAP: dict[str, set[MeterScaleType]] = {
METER_UNIT_MAP: dict[str, list[MeterScaleType]] = {
UnitOfElectricCurrent.AMPERE: METER_UNIT_AMPERE,
UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET,
UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER,
@@ -166,7 +170,7 @@ METER_UNIT_MAP: dict[str, set[MeterScaleType]] = {
UnitOfPower.WATT: METER_UNIT_WATT,
}
MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
MULTILEVEL_SENSOR_UNIT_MAP: dict[str, list[MultilevelSensorScaleType]] = {
UnitOfElectricCurrent.AMPERE: SENSOR_UNIT_AMPERE,
UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H,
UnitOfTemperature.CELSIUS: UNIT_CELSIUS,
@@ -174,17 +178,19 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE,
UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER,
UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR,
SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL,
UnitOfSoundPressure.DECIBEL: UNIT_DECIBEL,
UnitOfSoundPressure.WEIGHTED_DECIBEL_A: UNIT_A_WEIGHTED_DECIBELS,
DEGREE: UNIT_DEGREES,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: [
*UNIT_DENSITY,
*UNIT_MICROGRAM_PER_CUBIC_METER,
},
],
UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT,
UnitOfLength.FEET: UNIT_FEET,
UnitOfVolume.GALLONS: UNIT_GALLONS,
UnitOfFrequency.HERTZ: UNIT_HERTZ,
UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY,
UnitOfPressure.KPA: UNIT_KILOPASCAL,
UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
UnitOfMass.KILOGRAMS: UNIT_KILOGRAM,
UnitOfFrequency.KILOHERTZ: UNIT_KILOHERTZ,
@@ -197,7 +203,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH,
UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S,
CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION,
PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI},
PERCENTAGE: [*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI],
UnitOfMass.POUNDS: UNIT_POUNDS,
UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL,
@@ -206,6 +212,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
UnitOfElectricPotential.VOLT: SENSOR_UNIT_VOLT,
UnitOfPower.WATT: SENSOR_UNIT_WATT,
UnitOfIrradiance.WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER,
UV_INDEX: UNIT_UV_INDEX,
}
_LOGGER = logging.getLogger(__name__)
@@ -319,9 +326,9 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate):
enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType,
set_map: Mapping[
str,
set[MultilevelSensorType]
| set[MultilevelSensorScaleType]
| set[MeterScaleType],
list[MultilevelSensorType]
| list[MultilevelSensorScaleType]
| list[MeterScaleType],
],
) -> str | None:
"""Find a key in a set map that matches a given enum value."""

View File

@@ -3,7 +3,7 @@
"name": "Z-Wave",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.1"],
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.44.0"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["usb", "http", "websocket_api"],
"iot_class": "local_push",

View File

@@ -24,6 +24,18 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
@@ -76,98 +88,207 @@ STATUS_ICON: dict[NodeStatus, str] = {
}
ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = {
ENTITY_DESC_KEY_BATTERY: SensorEntityDescription(
# These descriptions should include device class.
ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
tuple[str, str], SensorEntityDescription
] = {
(ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription(
ENTITY_DESC_KEY_BATTERY,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
ENTITY_DESC_KEY_CURRENT: SensorEntityDescription(
(ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription(
ENTITY_DESC_KEY_CURRENT,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
),
ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription(
(ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription(
ENTITY_DESC_KEY_VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
device_class=SensorDeviceClass.ENERGY,
(
ENTITY_DESC_KEY_VOLTAGE,
UnitOfElectricPotential.MILLIVOLT,
): SensorEntityDescription(
ENTITY_DESC_KEY_VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
),
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription(
(
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
UnitOfEnergy.KILO_WATT_HOUR,
): SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
ENTITY_DESC_KEY_POWER: SensorEntityDescription(
(ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription(
ENTITY_DESC_KEY_POWER,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
(ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription(
ENTITY_DESC_KEY_POWER_FACTOR,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
ENTITY_DESC_KEY_CO: SensorEntityDescription(
(ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
ENTITY_DESC_KEY_CO,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
ENTITY_DESC_KEY_CO2: SensorEntityDescription(
(ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
ENTITY_DESC_KEY_CO2,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
(ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription(
ENTITY_DESC_KEY_HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
(ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription(
ENTITY_DESC_KEY_ILLUMINANCE,
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription(
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.KPA,
),
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.PSI,
),
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.INHG,
),
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.MMHG,
),
(
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
): SensorEntityDescription(
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
),
ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription(
(ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription(
ENTITY_DESC_KEY_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription(
(
ENTITY_DESC_KEY_TEMPERATURE,
UnitOfTemperature.FAHRENHEIT,
): SensorEntityDescription(
ENTITY_DESC_KEY_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
),
(
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
UnitOfTemperature.CELSIUS,
): SensorEntityDescription(
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=None,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
(
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
UnitOfTemperature.FAHRENHEIT,
): SensorEntityDescription(
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
),
}
# These descriptions are without device class.
ENTITY_DESCRIPTION_KEY_MAP = {
ENTITY_DESC_KEY_CO: SensorEntityDescription(
ENTITY_DESC_KEY_CO,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
ENTITY_DESC_KEY_HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
ENTITY_DESC_KEY_ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
ENTITY_DESC_KEY_POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription(
ENTITY_DESC_KEY_MEASUREMENT,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription(
ENTITY_DESC_KEY_TOTAL_INCREASING,
device_class=None,
state_class=SensorStateClass.TOTAL_INCREASING,
),
}
def get_entity_description(
data: NumericSensorDataTemplateData,
) -> SensorEntityDescription:
"""Return the entity description for the given data."""
data_description_key = data.entity_description_key or ""
data_unit = data.unit_of_measurement or ""
return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get(
(data_description_key, data_unit),
ENTITY_DESCRIPTION_KEY_MAP.get(
data_description_key,
SensorEntityDescription(
"base_sensor", native_unit_of_measurement=data.unit_of_measurement
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -187,9 +308,8 @@ async def async_setup_entry(
data: NumericSensorDataTemplateData = info.platform_data
else:
data = NumericSensorDataTemplateData()
entity_description = ENTITY_DESCRIPTION_KEY_MAP.get(
data.entity_description_key or "", SensorEntityDescription("base_sensor")
)
entity_description = get_entity_description(data)
if info.platform_hint == "string_sensor":
entities.append(
@@ -308,11 +428,9 @@ class ZWaveNumericSensor(ZwaveSensorBase):
@callback
def on_value_update(self) -> None:
"""Handle scale changes for this value on value updated event."""
self._attr_native_unit_of_measurement = (
NumericSensorDataTemplate()
.resolve_data(self.info.primary_value)
.unit_of_measurement
)
data = NumericSensorDataTemplate().resolve_data(self.info.primary_value)
self.entity_description = get_entity_description(data)
self._attr_native_unit_of_measurement = data.unit_of_measurement
@property
def native_value(self) -> float:
@@ -324,6 +442,8 @@ class ZWaveNumericSensor(ZwaveSensorBase):
@property
def native_unit_of_measurement(self) -> str | None:
"""Return unit of measurement the value is expressed in."""
if self.entity_description.native_unit_of_measurement is not None:
return self.entity_description.native_unit_of_measurement
if self._attr_native_unit_of_measurement is not None:
return self._attr_native_unit_of_measurement
if self.info.primary_value.metadata.unit is None:

View File

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

View File

@@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry"
EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated"
STORAGE_KEY = "core.device_registry"
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 4
STORAGE_VERSION_MINOR = 3
SAVE_DELAY = 10
CLEANUP_DELAY = 10
@@ -70,7 +70,6 @@ class DeviceEntryType(StrEnum):
class DeviceEntry:
"""Device Registry Entry."""
aliases: set[str] = attr.ib(factory=set)
area_id: str | None = attr.ib(default=None)
config_entries: set[str] = attr.ib(converter=set, factory=set)
configuration_url: str | None = attr.ib(default=None)
@@ -175,9 +174,6 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
# Version 1.3 adds hw_version
for device in old_data["devices"]:
device["hw_version"] = None
if old_minor_version < 4:
for device in old_data["devices"]:
device["aliases"] = []
if old_major_version > 1:
raise NotImplementedError
@@ -382,7 +378,6 @@ class DeviceRegistry:
device_id: str,
*,
add_config_entry_id: str | UndefinedType = UNDEFINED,
aliases: set[str] | UndefinedType = UNDEFINED,
area_id: str | None | UndefinedType = UNDEFINED,
configuration_url: str | None | UndefinedType = UNDEFINED,
disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED,
@@ -473,7 +468,6 @@ class DeviceRegistry:
old_values["identifiers"] = old.identifiers
for attr_name, value in (
("aliases", aliases),
("area_id", area_id),
("configuration_url", configuration_url),
("disabled_by", disabled_by),
@@ -552,7 +546,6 @@ class DeviceRegistry:
if data is not None:
for device in data["devices"]:
devices[device["id"]] = DeviceEntry(
aliases=set(device["aliases"]),
area_id=device["area_id"],
config_entries=set(device["config_entries"]),
configuration_url=device["configuration_url"],
@@ -600,7 +593,6 @@ class DeviceRegistry:
data["devices"] = [
{
"aliases": list(entry.aliases),
"area_id": entry.area_id,
"config_entries": list(entry.config_entries),
"configuration_url": entry.configuration_url,

View File

@@ -21,9 +21,9 @@ cryptography==38.0.3
dbus-fast==1.82.0
fnvhash==0.1.0
hass-nabucasa==0.61.0
home-assistant-bluetooth==1.9.0
home-assistant-frontend==20221228.0
httpx==0.23.1
home-assistant-bluetooth==1.9.2
home-assistant-frontend==20230104.0
httpx==0.23.2
ifaddr==0.1.7
janus==1.0.0
jinja2==3.1.2
@@ -90,7 +90,7 @@ regex==2021.8.28
# requirements so we can directly link HA versions to these library versions.
anyio==3.6.2
h11==0.14.0
httpcore==0.16.2
httpcore==0.16.3
# Ensure we have a hyperframe version that works in Python 3.10
# 5.2.0 fixed a collections abc deprecation

View File

@@ -3,7 +3,11 @@ from __future__ import annotations
from homeassistant.const import (
UNIT_NOT_RECOGNIZED_TEMPLATE,
UnitOfDataRate,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfInformation,
UnitOfLength,
UnitOfMass,
UnitOfPower,
@@ -86,6 +90,28 @@ class BaseUnitConverter:
return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit]
class DataRateConverter(BaseUnitConverter):
"""Utility to convert data rate values."""
UNIT_CLASS = "data_rate"
NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND
# Units in terms of bits
_UNIT_CONVERSION: dict[str, float] = {
UnitOfDataRate.BITS_PER_SECOND: 1,
UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3,
UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6,
UnitOfDataRate.GIGABITS_PER_SECOND: 1 / 1e9,
UnitOfDataRate.BYTES_PER_SECOND: 1 / 8,
UnitOfDataRate.KILOBYTES_PER_SECOND: 1 / 8e3,
UnitOfDataRate.MEGABYTES_PER_SECOND: 1 / 8e6,
UnitOfDataRate.GIGABYTES_PER_SECOND: 1 / 8e9,
UnitOfDataRate.KIBIBYTES_PER_SECOND: 1 / 2**13,
UnitOfDataRate.MEBIBYTES_PER_SECOND: 1 / 2**23,
UnitOfDataRate.GIBIBYTES_PER_SECOND: 1 / 2**33,
}
VALID_UNITS = set(UnitOfDataRate)
class DistanceConverter(BaseUnitConverter):
"""Utility to convert distance values."""
@@ -113,6 +139,33 @@ class DistanceConverter(BaseUnitConverter):
}
class ElectricCurrentConverter(BaseUnitConverter):
"""Utility to convert electric current values."""
UNIT_CLASS = "electric_current"
NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE
_UNIT_CONVERSION: dict[str, float] = {
UnitOfElectricCurrent.AMPERE: 1,
UnitOfElectricCurrent.MILLIAMPERE: 1e3,
}
VALID_UNITS = set(UnitOfElectricCurrent)
class ElectricPotentialConverter(BaseUnitConverter):
"""Utility to convert electric potential values."""
UNIT_CLASS = "voltage"
NORMALIZED_UNIT = UnitOfElectricPotential.VOLT
_UNIT_CONVERSION: dict[str, float] = {
UnitOfElectricPotential.VOLT: 1,
UnitOfElectricPotential.MILLIVOLT: 1e3,
}
VALID_UNITS = {
UnitOfElectricPotential.VOLT,
UnitOfElectricPotential.MILLIVOLT,
}
class EnergyConverter(BaseUnitConverter):
"""Utility to convert energy values."""
@@ -132,6 +185,38 @@ class EnergyConverter(BaseUnitConverter):
}
class InformationConverter(BaseUnitConverter):
"""Utility to convert information values."""
UNIT_CLASS = "information"
NORMALIZED_UNIT = UnitOfInformation.BITS
# Units in terms of bits
_UNIT_CONVERSION: dict[str, float] = {
UnitOfInformation.BITS: 1,
UnitOfInformation.KILOBITS: 1 / 1e3,
UnitOfInformation.MEGABITS: 1 / 1e6,
UnitOfInformation.GIGABITS: 1 / 1e9,
UnitOfInformation.BYTES: 1 / 8,
UnitOfInformation.KILOBYTES: 1 / 8e3,
UnitOfInformation.MEGABYTES: 1 / 8e6,
UnitOfInformation.GIGABYTES: 1 / 8e9,
UnitOfInformation.TERABYTES: 1 / 8e12,
UnitOfInformation.PETABYTES: 1 / 8e15,
UnitOfInformation.EXABYTES: 1 / 8e18,
UnitOfInformation.ZETTABYTES: 1 / 8e21,
UnitOfInformation.YOTTABYTES: 1 / 8e24,
UnitOfInformation.KIBIBYTES: 1 / 2**13,
UnitOfInformation.MEBIBYTES: 1 / 2**23,
UnitOfInformation.GIBIBYTES: 1 / 2**33,
UnitOfInformation.TEBIBYTES: 1 / 2**43,
UnitOfInformation.PEBIBYTES: 1 / 2**53,
UnitOfInformation.EXBIBYTES: 1 / 2**63,
UnitOfInformation.ZEBIBYTES: 1 / 2**73,
UnitOfInformation.YOBIBYTES: 1 / 2**83,
}
VALID_UNITS = set(UnitOfInformation)
class MassConverter(BaseUnitConverter):
"""Utility to convert mass values."""

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.1.0b2"
version = "2023.1.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -35,8 +35,8 @@ dependencies = [
"ciso8601==2.3.0",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.23.1",
"home-assistant-bluetooth==1.9.0",
"httpx==0.23.2",
"home-assistant-bluetooth==1.9.2",
"ifaddr==0.1.7",
"jinja2==3.1.2",
"lru-dict==1.1.8",

View File

@@ -10,8 +10,8 @@ awesomeversion==22.9.0
bcrypt==3.1.7
certifi>=2021.5.30
ciso8601==2.3.0
httpx==0.23.1
home-assistant-bluetooth==1.9.0
httpx==0.23.2
home-assistant-bluetooth==1.9.2
ifaddr==0.1.7
jinja2==3.1.2
lru-dict==1.1.8

View File

@@ -40,7 +40,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.33.0
PySwitchbot==0.36.3
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -86,7 +86,7 @@ adb-shell[async]==0.4.3
adext==0.4.2
# homeassistant.components.adguard
adguardhome==0.5.1
adguardhome==0.6.1
# homeassistant.components.advantage_air
advantage_air==0.4.1
@@ -288,7 +288,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==42
aiounifi==43
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@@ -419,10 +419,10 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10
# homeassistant.components.zha
bellows==0.34.5
bellows==0.34.6
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.4
bimmer_connected==0.12.0
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -488,7 +488,7 @@ brunt==1.2.0
bt_proximity==0.2.1
# homeassistant.components.bthome
bthome-ble==2.4.0
bthome-ble==2.4.1
# homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1
@@ -741,10 +741,10 @@ fritzconnection==1.10.3
gTTS==2.2.4
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.5
gassist-text==0.0.7
# homeassistant.components.google
gcal-sync==4.1.0
gcal-sync==4.1.1
# homeassistant.components.geniushub
geniushub-client==0.6.30
@@ -858,7 +858,7 @@ hass-nabucasa==0.61.0
hass_splunk==0.1.1
# homeassistant.components.tasmota
hatasmota==0.6.1
hatasmota==0.6.2
# homeassistant.components.jewish_calendar
hdate==0.10.4
@@ -888,7 +888,7 @@ hole==0.8.0
holidays==0.17.2
# homeassistant.components.frontend
home-assistant-frontend==20221228.0
home-assistant-frontend==20230104.0
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -930,7 +930,7 @@ ibm-watson==5.2.2
ibmiotf==0.3.4
# homeassistant.components.local_calendar
ical==4.2.8
ical==4.2.9
# homeassistant.components.ping
icmplib==3.0
@@ -1029,7 +1029,7 @@ librouteros==3.2.0
libsoundtouch==0.8
# homeassistant.components.life360
life360==5.3.0
life360==5.5.0
# homeassistant.components.osramlightify
lightify==1.0.7.3
@@ -1119,7 +1119,7 @@ moat-ble==0.1.1
moehlenhoff-alpha2==1.2.1
# homeassistant.components.motion_blinds
motionblinds==0.6.13
motionblinds==0.6.15
# homeassistant.components.motioneye
motioneye-client==0.3.12
@@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0
pySwitchmate==0.5.1
# homeassistant.components.tibber
pyTibber==0.26.6
pyTibber==0.26.7
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1473,7 +1473,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==7.4.0
pyatmo==7.5.0
# homeassistant.components.atome
pyatome==0.1.1
@@ -1500,10 +1500,10 @@ pyblackbird==0.5
pybotvac==0.0.23
# homeassistant.components.braviatv
pybravia==0.2.3
pybravia==0.2.5
# homeassistant.components.nissan_leaf
pycarwings2==2.13
pycarwings2==2.14
# homeassistant.components.cloudflare
pycfdns==2.0.1
@@ -1542,7 +1542,7 @@ pydaikin==2.8.0
pydanfossair==0.1.0
# homeassistant.components.deconz
pydeconz==105
pydeconz==106
# homeassistant.components.delijn
pydelijn==1.0.0
@@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0
pyebox==1.1.4
# homeassistant.components.econet
pyeconet==0.1.15
pyeconet==0.1.18
# homeassistant.components.edimax
pyedimax==0.2.1
@@ -1832,7 +1832,7 @@ pyownet==0.10.0.post1
pypca==0.0.7
# homeassistant.components.lcn
pypck==0.7.15
pypck==0.7.16
# homeassistant.components.pjlink
pypjlink2==1.2.1
@@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11
# homeassistant.components.reolink
reolink-ip==0.0.40
reolink-aio==0.1.3
# homeassistant.components.python_script
restrictedpython==5.2
@@ -2220,7 +2220,7 @@ rokuecp==0.17.0
roombapy==1.6.5
# homeassistant.components.roon
roonapi==0.1.1
roonapi==0.1.2
# homeassistant.components.rova
rova==0.3.0
@@ -2308,7 +2308,7 @@ sisyphus-control==3.1.2
slackclient==2.5.0
# homeassistant.components.xmpp
slixmpp==1.8.2
slixmpp==1.8.3
# homeassistant.components.smart_meter_texas
smart-meter-texas==0.4.7
@@ -2647,7 +2647,7 @@ zengge==0.2
zeroconf==0.47.1
# homeassistant.components.zha
zha-quirks==0.0.89
zha-quirks==0.0.90
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2668,13 +2668,13 @@ zigpy-zigate==0.10.3
zigpy-znp==0.9.2
# homeassistant.components.zha
zigpy==0.52.3
zigpy==0.53.0
# homeassistant.components.zoneminder
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.43.1
zwave-js-server-python==0.44.0
# homeassistant.components.zwave_me
zwave_me_ws==0.3.0

View File

@@ -36,7 +36,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.33.0
PySwitchbot==0.36.3
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -76,7 +76,7 @@ adb-shell[async]==0.4.3
adext==0.4.2
# homeassistant.components.adguard
adguardhome==0.5.1
adguardhome==0.6.1
# homeassistant.components.advantage_air
advantage_air==0.4.1
@@ -263,7 +263,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==42
aiounifi==43
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@@ -346,10 +346,10 @@ base36==0.1.1
beautifulsoup4==4.11.1
# homeassistant.components.zha
bellows==0.34.5
bellows==0.34.6
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.4
bimmer_connected==0.12.0
# homeassistant.components.bluetooth
bleak-retry-connector==2.13.0
@@ -392,7 +392,7 @@ brother==2.1.1
brunt==1.2.0
# homeassistant.components.bthome
bthome-ble==2.4.0
bthome-ble==2.4.1
# homeassistant.components.buienradar
buienradar==1.0.5
@@ -557,10 +557,10 @@ fritzconnection==1.10.3
gTTS==2.2.4
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.5
gassist-text==0.0.7
# homeassistant.components.google
gcal-sync==4.1.0
gcal-sync==4.1.1
# homeassistant.components.geocaching
geocachingapi==0.2.1
@@ -647,7 +647,7 @@ habitipy==0.2.0
hass-nabucasa==0.61.0
# homeassistant.components.tasmota
hatasmota==0.6.1
hatasmota==0.6.2
# homeassistant.components.jewish_calendar
hdate==0.10.4
@@ -668,7 +668,7 @@ hole==0.8.0
holidays==0.17.2
# homeassistant.components.frontend
home-assistant-frontend==20221228.0
home-assistant-frontend==20230104.0
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -695,7 +695,7 @@ iaqualink==0.5.0
ibeacon_ble==1.0.1
# homeassistant.components.local_calendar
ical==4.2.8
ical==4.2.9
# homeassistant.components.ping
icmplib==3.0
@@ -767,7 +767,7 @@ librouteros==3.2.0
libsoundtouch==0.8
# homeassistant.components.life360
life360==5.3.0
life360==5.5.0
# homeassistant.components.logi_circle
logi_circle==0.2.3
@@ -821,7 +821,7 @@ moat-ble==0.1.1
moehlenhoff-alpha2==1.2.1
# homeassistant.components.motion_blinds
motionblinds==0.6.13
motionblinds==0.6.15
# homeassistant.components.motioneye
motioneye-client==0.3.12
@@ -1039,7 +1039,7 @@ pyMetno==0.9.0
pyRFXtrx==0.30.0
# homeassistant.components.tibber
pyTibber==0.26.6
pyTibber==0.26.7
# homeassistant.components.nextbus
py_nextbusnext==0.1.5
@@ -1061,7 +1061,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==7.4.0
pyatmo==7.5.0
# homeassistant.components.apple_tv
pyatv==0.10.3
@@ -1079,7 +1079,7 @@ pyblackbird==0.5
pybotvac==0.0.23
# homeassistant.components.braviatv
pybravia==0.2.3
pybravia==0.2.5
# homeassistant.components.cloudflare
pycfdns==2.0.1
@@ -1097,7 +1097,7 @@ pycoolmasternet-async==0.1.2
pydaikin==2.8.0
# homeassistant.components.deconz
pydeconz==105
pydeconz==106
# homeassistant.components.dexcom
pydexcom==0.2.3
@@ -1106,7 +1106,7 @@ pydexcom==0.2.3
pydroid-ipcam==2.0.0
# homeassistant.components.econet
pyeconet==0.1.15
pyeconet==0.1.18
# homeassistant.components.efergy
pyefergy==22.1.1
@@ -1306,7 +1306,7 @@ pyowm==3.2.0
pyownet==0.10.0.post1
# homeassistant.components.lcn
pypck==0.7.15
pypck==0.7.16
# homeassistant.components.plaato
pyplaato==0.0.18
@@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11
# homeassistant.components.reolink
reolink-ip==0.0.40
reolink-aio==0.1.3
# homeassistant.components.python_script
restrictedpython==5.2
@@ -1547,7 +1547,7 @@ rokuecp==0.17.0
roombapy==1.6.5
# homeassistant.components.roon
roonapi==0.1.1
roonapi==0.1.2
# homeassistant.components.rpi_power
rpi-bad-power==0.1.0
@@ -1854,7 +1854,7 @@ zamg==0.2.2
zeroconf==0.47.1
# homeassistant.components.zha
zha-quirks==0.0.89
zha-quirks==0.0.90
# homeassistant.components.zha
zigpy-deconz==0.19.2
@@ -1869,10 +1869,10 @@ zigpy-zigate==0.10.3
zigpy-znp==0.9.2
# homeassistant.components.zha
zigpy==0.52.3
zigpy==0.53.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.43.1
zwave-js-server-python==0.44.0
# homeassistant.components.zwave_me
zwave_me_ws==0.3.0

View File

@@ -101,7 +101,7 @@ regex==2021.8.28
# requirements so we can directly link HA versions to these library versions.
anyio==3.6.2
h11==0.14.0
httpcore==0.16.2
httpcore==0.16.3
# Ensure we have a hyperframe version that works in Python 3.10
# 5.2.0 fixed a collections abc deprecation

View File

@@ -1,6 +1,6 @@
"""Define test fixtures for AirVisual."""
import json
from unittest.mock import patch
from unittest.mock import AsyncMock, Mock, patch
import pytest
@@ -56,17 +56,27 @@ def data_fixture():
return json.loads(load_fixture("data.json", "airvisual"))
@pytest.fixture(name="pro_data", scope="session")
def pro_data_fixture():
"""Define an update coordinator data example for the Pro."""
return json.loads(load_fixture("data.json", "airvisual_pro"))
@pytest.fixture(name="pro")
def pro_fixture(pro_data):
"""Define a mocked NodeSamba object."""
return Mock(
async_connect=AsyncMock(),
async_disconnect=AsyncMock(),
async_get_latest_measurements=AsyncMock(return_value=pro_data),
)
@pytest.fixture(name="setup_airvisual")
async def setup_airvisual_fixture(hass, config, data):
"""Define a fixture to set up AirVisual."""
with patch("pyairvisual.air_quality.AirQuality.city"), patch(
"pyairvisual.air_quality.AirQuality.nearest_city", return_value=data
), patch("pyairvisual.node.NodeSamba.async_connect"), patch(
"pyairvisual.node.NodeSamba.async_get_latest_measurements"
), patch(
"pyairvisual.node.NodeSamba.async_disconnect"
), patch(
"homeassistant.components.airvisual.PLATFORMS", []
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()

View File

@@ -1,5 +1,5 @@
"""Define tests for the AirVisual config flow."""
from unittest.mock import Mock, patch
from unittest.mock import patch
from pyairvisual.cloud_api import (
InvalidKeyError,
@@ -21,6 +21,7 @@ from homeassistant.components.airvisual import (
INTEGRATION_TYPE_GEOGRAPHY_NAME,
INTEGRATION_TYPE_NODE_PRO,
)
from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import (
CONF_API_KEY,
@@ -31,8 +32,7 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP,
CONF_STATE,
)
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from tests.common import MockConfigEntry
@@ -169,42 +169,41 @@ async def test_migration_1_2(hass, config, config_entry, setup_airvisual, unique
}
@pytest.mark.parametrize(
"config,config_entry_version,unique_id",
[
(
{
CONF_IP_ADDRESS: "192.168.1.100",
CONF_PASSWORD: "abcde12345",
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO,
},
2,
"192.16.1.100",
)
],
)
async def test_migration_2_3(hass, config, config_entry, unique_id):
async def test_migration_2_3(hass, pro):
"""Test migrating from version 2 to 3."""
old_pro_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="192.168.1.100",
data={
CONF_IP_ADDRESS: "192.168.1.100",
CONF_PASSWORD: "abcde12345",
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO,
},
version=2,
)
old_pro_entry.add_to_hass(hass)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
name="192.168.1.100",
config_entry_id=old_pro_entry.entry_id,
identifiers={(DOMAIN, "ABCDE12345")},
)
with patch(
"homeassistant.components.airvisual.automation.automations_with_device",
return_value=["automation.test_automation"],
), patch(
"homeassistant.components.airvisual.async_get_pro_config_entry_by_ip_address",
return_value=MockConfigEntry(
domain="airvisual_pro",
unique_id="192.168.1.100",
data={CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "abcde12345"},
version=3,
),
"homeassistant.components.airvisual_pro.NodeSamba", return_value=pro
), patch(
"homeassistant.components.airvisual.async_get_pro_device_by_config_entry",
return_value=Mock(id="abcde12345"),
"homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.config_entries.async_setup(old_pro_entry.entry_id)
await hass.async_block_till_done()
airvisual_config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(airvisual_config_entries) == 0
for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)):
entries = hass.config_entries.async_entries(domain)
assert len(entries) == entry_count
issue_registry = ir.async_get(hass)
assert len(issue_registry.issues) == 1

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