Compare commits

...

57 Commits

Author SHA1 Message Date
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
Paulus Schoutsen d9aff9d7b0 Bumped version to 2023.1.0b2 2022-12-29 20:55:27 -05:00
shbatm 46e11c2fa8 ISY994: Bump PyISY to 3.0.10 (#84821) 2022-12-29 20:54:48 -05:00
J. Nick Koston 522477d5a4 Fix thermobeacon WS08 models that identify with manufacturer_id 27 (#84812)
fixes #84706
2022-12-29 20:54:47 -05:00
Joakim Plate cc2b592221 Correct missing alarm reset button on nibe (#84809)
fixes undefined
2022-12-29 20:54:46 -05:00
Aaron Bach bd86111dd8 Fix AirVisual Pro sensors with incorrect units for their device classes (#84800) 2022-12-29 20:54:45 -05:00
Michael f0514008fa Deprecate YAML config in PI-Hole (#84797)
create an issue about deprecated yaml config
2022-12-29 20:54:44 -05:00
Aaron Bach 3c8a66abbe Don't attempt setup on migrated AirVisual Pro in the airvisual domain (#84796)
fixes undefined
2022-12-29 20:54:44 -05:00
Aaron Bach f1d509be03 Remove ozone device class from OpenUV sensor (#84791)
fixes undefined
2022-12-29 20:54:43 -05:00
Robert Svensson fbdc7d44bc Only subscribe to specific UniFi object ID (#84787) 2022-12-29 20:54:42 -05:00
Hans Oischinger cae386465e Catch vicare errors when deactivating preset fails (#84778)
vicare: catch errors when deactivating preset fails
2022-12-29 20:54:41 -05:00
Allen Porter 31847e3a69 Check google calendar API scope to determine if write access is enabled (#84749)
* Check google calendar API scope to determine if write access is enabled

* Add API scope for calendar service for creating events
2022-12-29 20:54:40 -05:00
Jc2k 5787e1506c Fix UUID normalisation for vendor extensions in homekit_controller thread transport (#84746) 2022-12-29 20:54:39 -05:00
starkillerOG 9a29f64128 Bump pynetgear to 0.10.9 (#84733) 2022-12-29 20:54:38 -05:00
Tomas Kislan 6c0e4a9e8f Fix and upgrade minio integration (#84545)
closes https://github.com/home-assistant/core/issues/79842
2022-12-29 20:54:37 -05:00
Franck Nijhof 9ce64f8990 Bumped version to 2023.1.0b1 2022-12-28 22:18:06 +01:00
Aaron Bach 6ef4086683 Fix incorrect values for AirVisual Pro sensors (#84725) 2022-12-28 22:16:22 +01:00
Aaron Bach b146f52317 Remove incorrect unit for AirVisual AQI sensor (#84723)
fixes undefined
2022-12-28 22:16:19 +01:00
Hans Oischinger 001bd78bcb water_heater: Add unsupported states (#84720) 2022-12-28 22:16:16 +01:00
Allen Porter ba4ec8f8c1 Gracefully handle caldav event with missing summary (#84719)
fixes undefined
2022-12-28 22:16:12 +01:00
Michael 8aa3a6cc15 Remove deprecated tankerkoenig YAML config (#84711)
remove yaml import
2022-12-28 22:16:09 +01:00
Hmmbob ed43e1d3a4 Update apprise to 1.2.1 (#84705) 2022-12-28 22:16:06 +01:00
Marcel van der Veldt b7654c0fce Bump python matter server to 1.0.8 (#84692)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-12-28 22:16:02 +01:00
Franck Nijhof 99f3cfdf8a Bumped version to 2023.1.0b0 2022-12-28 16:51:38 +01:00
93 changed files with 1398 additions and 821 deletions
+67 -40
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
@@ -207,6 +178,11 @@ def _standardize_geography_config_entry(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AirVisual as config entry."""
if CONF_API_KEY not in entry.data:
# If this is a migrated AirVisual Pro entry, there's no actual setup to do;
# that will be handled by the `airvisual_pro` domain:
return False
_standardize_geography_config_entry(hass, entry)
websession = aiohttp_client.async_get_clientsession(hass)
@@ -301,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(
@@ -319,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,
@@ -69,7 +69,6 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = (
key=SENSOR_KIND_AQI,
name="Air quality index",
device_class=SensorDeviceClass.AQI,
native_unit_of_measurement="AQI",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
@@ -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)
@@ -39,7 +39,6 @@ SENSOR_DESCRIPTIONS = (
key=SENSOR_KIND_AQI,
name="Air quality index",
device_class=SensorDeviceClass.AQI,
native_unit_of_measurement="AQI",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
@@ -94,7 +93,7 @@ SENSOR_DESCRIPTIONS = (
key=SENSOR_KIND_VOC,
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
)
@@ -159,6 +158,6 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity):
elif self.entity_description.key == SENSOR_KIND_BATTERY_LEVEL:
self._attr_native_value = self.status["battery"]
else:
self._attr_native_value = self.MEASUREMENTS_KEY_TO_VALUE[
self.entity_description.key
self._attr_native_value = self.measurements[
self.MEASUREMENTS_KEY_TO_VALUE[self.entity_description.key]
]
@@ -2,7 +2,7 @@
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/integrations/apprise",
"requirements": ["apprise==1.2.0"],
"requirements": ["apprise==1.2.1"],
"codeowners": ["@caronc"],
"iot_class": "cloud_push",
"loggers": ["apprise"]
@@ -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):
@@ -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": [
{
+16 -11
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": {
@@ -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": {
+22 -12
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
@@ -184,9 +184,9 @@ class WebDavCalendarData:
continue
event_list.append(
CalendarEvent(
summary=vevent.summary.value,
start=vevent.dtstart.value,
end=self.get_end_date(vevent),
summary=self.get_attr_value(vevent, "summary") or "",
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"),
)
@@ -264,11 +264,13 @@ class WebDavCalendarData:
return
# Populate the entity attributes with the event values
(summary, offset) = extract_offset(vevent.summary.value, OFFSET)
(summary, offset) = extract_offset(
self.get_attr_value(vevent, "summary") or "", OFFSET
)
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"),
)
@@ -306,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."""
@@ -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,
}
)
}
@@ -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",
+30 -7
View File
@@ -401,7 +401,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 +445,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 +477,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 +492,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 +547,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 +564,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 +586,11 @@ 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 native_value(self) -> StateType:
"""Return the state of sensor, if available, translate if needed."""
@@ -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.17"],
"codeowners": ["@vangorra", "@w1ll1am23"],
"iot_class": "cloud_push",
"loggers": ["paho_mqtt", "pyeconet"]
@@ -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==20221230.0"],
"dependencies": [
"api",
"auth",
+11 -4
View File
@@ -63,6 +63,7 @@ from . import (
load_config,
update_config,
)
from .api import get_feature_access
from .const import (
DATA_SERVICE,
DATA_STORE,
@@ -74,6 +75,7 @@ from .const import (
EVENT_START_DATE,
EVENT_START_DATETIME,
EVENT_TYPES_CONF,
FeatureAccess,
)
_LOGGER = logging.getLogger(__name__)
@@ -213,11 +215,13 @@ async def async_setup_entry(
# Prefer calendar sync down of resources when possible. However, sync does not work
# for search. Also free-busy calendars denormalize recurring events as individual
# events which is not efficient for sync
support_write = calendar_item.access_role.is_writer
support_write = (
calendar_item.access_role.is_writer
and get_feature_access(hass, config_entry) is FeatureAccess.read_write
)
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,
@@ -265,7 +269,10 @@ async def async_setup_entry(
await hass.async_add_executor_job(append_calendars_to_config)
platform = entity_platform.async_get_current_platform()
if any(calendar_item.access_role.is_writer for calendar_item in result.items):
if (
any(calendar_item.access_role.is_writer for calendar_item in result.items)
and get_feature_access(hass, config_entry) is FeatureAccess.read_write
):
platform.async_register_entity_service(
SERVICE_CREATE_EVENT,
CREATE_EVENT_SCHEMA,
@@ -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):
@@ -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"
@@ -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]
@@ -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)
@@ -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)
@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==2.4.2"],
"requirements": ["aiohomekit==2.4.3"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"],
@@ -3,7 +3,7 @@
"name": "Universal Devices ISY994",
"integration_type": "hub",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.9"],
"requirements": ["pyisy==3.0.10"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [
+1 -2
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from typing import cast
import async_timeout
from matter_server.client import MatterClient
@@ -245,7 +244,7 @@ def _async_init_services(hass: HomeAssistant) -> None:
# This could be more efficient
for node in await matter_client.get_nodes():
if node.unique_id == unique_id:
return cast(int, node.node_id)
return node.node_id
return None
+8 -3
View File
@@ -47,8 +47,12 @@ class MatterAdapter:
for node in await self.matter_client.get_nodes():
self._setup_node(node)
def node_added_callback(event: EventType, node: MatterNode) -> None:
def node_added_callback(event: EventType, node: MatterNode | None) -> None:
"""Handle node added event."""
if node is None:
# We can clean this up when we've improved the typing in the library.
# https://github.com/home-assistant-libs/python-matter-server/pull/153
raise RuntimeError("Node added event without node")
self._setup_node(node)
self.config_entry.async_on_unload(
@@ -61,8 +65,9 @@ class MatterAdapter:
bridge_unique_id: str | None = None
if node.aggregator_device_type_instance is not None:
node_info = node.root_device_type_instance.get_cluster(all_clusters.Basic)
if node.aggregator_device_type_instance is not None and (
node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic)
):
self._create_device_registry(
node_info, node_info.nodeLabel or "Hub device", None
)
@@ -39,9 +39,8 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
self._attr_is_on = self._device_type_instance.get_cluster(
clusters.BooleanState
).stateValue
cluster = self._device_type_instance.get_cluster(clusters.BooleanState)
self._attr_is_on = cluster.stateValue if cluster else None
class MatterOccupancySensor(MatterBinarySensor):
@@ -52,11 +51,9 @@ class MatterOccupancySensor(MatterBinarySensor):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
occupancy = self._device_type_instance.get_cluster(
clusters.OccupancySensing
).occupancy
cluster = self._device_type_instance.get_cluster(clusters.OccupancySensing)
# The first bit = if occupied
self._attr_is_on = occupancy & 1 == 1
self._attr_is_on = cluster.occupancy & 1 == 1 if cluster else None
@dataclass
@@ -20,6 +20,7 @@ from homeassistant.components.hassio import (
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
from .addon import get_addon_manager
@@ -131,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
await self.start_task
except (CannotConnect, AddonError, AbortFlow) as err:
except (FailedConnect, AddonError, AbortFlow) as err:
self.start_task = None
LOGGER.error(err)
return self.async_show_progress_done(next_step_id="start_failed")
@@ -170,7 +171,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else:
break
else:
raise CannotConnect("Failed to start Matter Server add-on: timeout")
raise FailedConnect("Failed to start Matter Server add-on: timeout")
finally:
# Continue the flow after show progress when the task is done.
self.hass.async_create_task(
@@ -324,3 +325,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon,
},
)
class FailedConnect(HomeAssistantError):
"""Failed to connect to the Matter Server."""
+9 -1
View File
@@ -59,7 +59,15 @@ class MatterEntity(Entity):
self._unsubscribes: list[Callable] = []
# for fast lookups we create a mapping to the attribute paths
self._attributes_map: dict[type, str] = {}
self._attr_unique_id = f"{matter_client.server_info.compressed_fabric_id}-{node.unique_id}-{device_type_instance.endpoint}-{device_type_instance.device_type.device_type}"
server_info = matter_client.server_info
# The server info is set when the client connects to the server.
assert server_info is not None
self._attr_unique_id = (
f"{server_info.compressed_fabric_id}-"
f"{node.unique_id}-"
f"{device_type_instance.endpoint}-"
f"{device_type_instance.device_type.device_type}"
)
@property
def device_info(self) -> DeviceInfo | None:
+10 -7
View File
@@ -57,6 +57,9 @@ class MatterLight(MatterEntity, LightEntity):
return
level_control = self._device_type_instance.get_cluster(clusters.LevelControl)
# We check above that the device supports brightness, ie level control.
assert level_control is not None
level = round(
renormalize(
kwargs[ATTR_BRIGHTNESS],
@@ -86,20 +89,20 @@ class MatterLight(MatterEntity, LightEntity):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
if self._attr_supported_color_modes is None:
if self._supports_brightness():
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
supports_brigthness = self._supports_brightness()
if self._attr_supported_color_modes is None and supports_brigthness:
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
if attr := self.get_matter_attribute(clusters.OnOff.Attributes.OnOff):
self._attr_is_on = attr.value
if (
clusters.LevelControl.Attributes.CurrentLevel
in self.entity_description.subscribe_attributes
):
if supports_brigthness:
level_control = self._device_type_instance.get_cluster(
clusters.LevelControl
)
# We check above that the device supports brightness, ie level control.
assert level_control is not None
# Convert brightness to Home Assistant = 0..255
self._attr_brightness = round(
@@ -3,7 +3,7 @@
"name": "Matter (BETA)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/matter",
"requirements": ["python-matter-server==1.0.7"],
"requirements": ["python-matter-server==1.0.8"],
"dependencies": ["websocket_api"],
"codeowners": ["@home-assistant/matter"],
"iot_class": "local_push"
+2 -1
View File
@@ -56,7 +56,8 @@ class MatterSwitch(MatterEntity, SwitchEntity):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
self._attr_is_on = self._device_type_instance.get_cluster(clusters.OnOff).onOff
cluster = self._device_type_instance.get_cluster(clusters.OnOff)
self._attr_is_on = cluster.onOff if cluster else None
@dataclass
+2 -4
View File
@@ -136,8 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
file_path = _render_service_value(service, ATTR_FILE_PATH)
if not hass.config.is_allowed_path(file_path):
_LOGGER.error("Invalid file_path %s", file_path)
return
raise ValueError(f"Invalid file_path {file_path}")
minio_client.fput_object(bucket, key, file_path)
@@ -148,8 +147,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
file_path = _render_service_value(service, ATTR_FILE_PATH)
if not hass.config.is_allowed_path(file_path):
_LOGGER.error("Invalid file_path %s", file_path)
return
raise ValueError(f"Invalid file_path {file_path}")
minio_client.fget_object(bucket, key, file_path)
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "minio",
"name": "Minio",
"documentation": "https://www.home-assistant.io/integrations/minio",
"requirements": ["minio==5.0.10"],
"requirements": ["minio==7.1.12"],
"codeowners": ["@tkislan"],
"iot_class": "cloud_push",
"loggers": ["minio"]
@@ -34,7 +34,9 @@ def create_minio_client(
endpoint: str, access_key: str, secret_key: str, secure: bool
) -> Minio:
"""Create Minio client."""
return Minio(endpoint, access_key, secret_key, secure)
return Minio(
endpoint=endpoint, access_key=access_key, secret_key=secret_key, secure=secure
)
def get_minio_notification_response(
@@ -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.14"],
"dependencies": ["network"],
"dhcp": [
{ "registered_devices": true },
@@ -2,7 +2,7 @@
"domain": "netgear",
"name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.10.8"],
"requirements": ["pynetgear==0.10.9"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling",
"config_flow": true,
@@ -24,11 +24,9 @@ async def async_setup_entry(
coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id]
def reset_buttons():
for entity_description in UNIT_COILGROUPS.get(coordinator.series, {}).get(
"main"
):
if unit := UNIT_COILGROUPS.get(coordinator.series, {}).get("main"):
try:
yield NibeAlarmResetButton(coordinator, entity_description)
yield NibeAlarmResetButton(coordinator, unit)
except CoilNotFoundException as exception:
LOGGER.debug("Skipping button %r", exception)
@@ -46,6 +44,7 @@ class NibeAlarmResetButton(CoordinatorEntity[Coordinator], ButtonEntity):
self._reset_coil = coordinator.heatpump.get_coil_by_address(unit.alarm_reset)
self._alarm_coil = coordinator.heatpump.get_coil_by_address(unit.alarm)
super().__init__(coordinator, {self._alarm_coil.address})
self._attr_name = self._reset_coil.title
self._attr_unique_id = f"{coordinator.unique_id}-alarm_reset"
self._attr_device_info = coordinator.device_info
@@ -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"]
+2 -2
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"
@@ -296,7 +296,7 @@ class NumberDeviceClass(StrEnum):
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`
Unit of measurement: `V`, `mV`
"""
VOLUME = "volume"
@@ -2,7 +2,6 @@
from __future__ import annotations
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
@@ -51,7 +50,6 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_CURRENT_OZONE_LEVEL,
name="Current ozone level",
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement="du",
state_class=SensorStateClass.MEASUREMENT,
),
+19 -6
View File
@@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -69,14 +70,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2023.2.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)
# import
if DOMAIN in config:
for conf in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
for conf in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True
@@ -25,5 +25,11 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"issues": {
"deprecated_yaml": {
"title": "The PI-Hole YAML configuration is being removed",
"description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
}
}
@@ -25,5 +25,11 @@
}
}
}
},
"issues": {
"deprecated_yaml": {
"description": "Die Konfiguration von PI-Hole mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die PI-Hole-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.",
"title": "Die PI-Hole YAML-Konfiguration wird entfernt"
}
}
}
@@ -25,5 +25,11 @@
}
}
}
},
"issues": {
"deprecated_yaml": {
"description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"title": "The PI-Hole YAML configuration is being removed"
}
}
}
@@ -74,8 +74,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
]
+2 -3
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,
),
+26 -17
View File
@@ -12,16 +12,19 @@ import async_timeout
from reolink_ip.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
+16 -9
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"
+30 -29
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
import logging
from typing import cast
from typing import Any
from reolink_ip.exceptions import ApiError, CredentialsInvalidError
import voluptuous as vol
@@ -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):
@@ -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
+21 -23
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
+12 -15
View File
@@ -2,7 +2,9 @@
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
@@ -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
@@ -4,8 +4,6 @@
"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"],
"iot_class": "local_polling",
"loggers": ["reolink-ip"]
+1 -1
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"]
+13 -4
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"
@@ -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),
@@ -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
+3 -2
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")
@@ -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,
@@ -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,58 @@ 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
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:
errors = {"base": "auth_failed"}
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),
},
)
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 +221,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 +289,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 +301,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()
@@ -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.1"],
"config_flow": true,
"dependencies": ["bluetooth"],
"codeowners": [
@@ -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.",
"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"
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
@@ -7,11 +7,36 @@
"switchbot_unsupported_type": "Unsupported Switchbot Type.",
"unknown": "Unexpected error"
},
"error": {
"auth_failed": "Authentication failed",
"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."
},
"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"
@@ -7,110 +7,28 @@ from math import ceil
import pytankerkoenig
from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_ID,
CONF_API_KEY,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
CONF_SCAN_INTERVAL,
CONF_SHOW_ON_MAP,
Platform,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ID, CONF_API_KEY, CONF_SHOW_ON_MAP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import (
CONF_FUEL_TYPES,
CONF_STATIONS,
DEFAULT_RADIUS,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
FUEL_TYPES,
)
from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.time_period,
vol.Optional(CONF_FUEL_TYPES, default=FUEL_TYPES): vol.All(
cv.ensure_list, [vol.In(FUEL_TYPES)]
),
vol.Inclusive(
CONF_LATITUDE,
"coordinates",
"Latitude and longitude must exist together",
): cv.latitude,
vol.Inclusive(
CONF_LONGITUDE,
"coordinates",
"Latitude and longitude must exist together",
): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.All(
cv.positive_int, vol.Range(min=1)
),
vol.Optional(CONF_STATIONS, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
}
)
},
),
extra=vol.ALLOW_EXTRA,
)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set the tankerkoenig component up."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_NAME: "Home",
CONF_API_KEY: conf[CONF_API_KEY],
CONF_FUEL_TYPES: conf[CONF_FUEL_TYPES],
CONF_LOCATION: {
"latitude": conf.get(CONF_LATITUDE, hass.config.latitude),
"longitude": conf.get(CONF_LONGITUDE, hass.config.longitude),
},
CONF_RADIUS: conf[CONF_RADIUS],
CONF_STATIONS: conf[CONF_STATIONS],
CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP],
},
)
)
return True
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -67,37 +67,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Import YAML configuration."""
await self.async_set_unique_id(
f"{config[CONF_LOCATION][CONF_LATITUDE]}_{config[CONF_LOCATION][CONF_LONGITUDE]}"
)
self._abort_if_unique_id_configured()
selected_station_ids: list[str] = []
# add all nearby stations
nearby_stations = await async_get_nearby_stations(self.hass, config)
for station in nearby_stations.get("stations", []):
selected_station_ids.append(station["id"])
# add all manual added stations
for station_id in config[CONF_STATIONS]:
selected_station_ids.append(station_id)
return self._create_entry(
data={
CONF_NAME: "Home",
CONF_API_KEY: config[CONF_API_KEY],
CONF_FUEL_TYPES: config[CONF_FUEL_TYPES],
CONF_LOCATION: config[CONF_LOCATION],
CONF_RADIUS: config[CONF_RADIUS],
CONF_STATIONS: selected_station_ids,
},
options={
CONF_SHOW_ON_MAP: config[CONF_SHOW_ON_MAP],
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -28,9 +28,15 @@
"manufacturer_data_start": [0],
"connectable": false
},
{
"service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb",
"manufacturer_id": 27,
"manufacturer_data_start": [0],
"connectable": false
},
{ "local_name": "ThermoBeacon", "connectable": false }
],
"requirements": ["thermobeacon-ble==0.4.0"],
"requirements": ["thermobeacon-ble==0.6.0"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"
+1
View File
@@ -83,6 +83,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
self.async_on_remove(
handler.subscribe(
self.async_signalling_callback,
id_filter=self._obj_id,
)
)
+1 -1
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": [
+5 -1
View File
@@ -6,6 +6,7 @@ import logging
from typing import Any
from PyViCare.PyViCareUtils import (
PyViCareCommandError,
PyViCareInvalidDataError,
PyViCareNotSupportedFeatureError,
PyViCareRateLimitError,
@@ -354,7 +355,10 @@ class ViCareClimate(ClimateEntity):
_LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program)
if self._current_program != VICARE_PROGRAM_NORMAL:
# We can't deactivate "normal"
self._circuit.deactivateProgram(self._current_program)
try:
self._circuit.deactivateProgram(self._current_program)
except PyViCareCommandError:
_LOGGER.debug("Unable to deactivate program %s", self._current_program)
if vicare_program != VICARE_PROGRAM_NORMAL:
# And we can't explicitly activate normal, either
self._circuit.activateProgram(vicare_program)
@@ -36,7 +36,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
VICARE_MODE_DHW = "dhw"
VICARE_MODE_HEATING = "heating"
VICARE_MODE_DHWANDHEATING = "dhwAndHeating"
VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling"
VICARE_MODE_FORCEDREDUCED = "forcedReduced"
VICARE_MODE_FORCEDNORMAL = "forcedNormal"
VICARE_MODE_OFF = "standby"
@@ -50,6 +52,8 @@ OPERATION_MODE_OFF = "off"
VICARE_TO_HA_HVAC_DHW = {
VICARE_MODE_DHW: OPERATION_MODE_ON,
VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON,
VICARE_MODE_DHWANDHEATINGCOOLING: OPERATION_MODE_ON,
VICARE_MODE_HEATING: OPERATION_MODE_OFF,
VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF,
VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON,
VICARE_MODE_OFF: OPERATION_MODE_OFF,
+1 -1
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 = "0.dev0"
PATCH_VERSION: Final = "0b4"
__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)
+9
View File
@@ -385,6 +385,15 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
"manufacturer_id": 24,
"service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb",
},
{
"connectable": False,
"domain": "thermobeacon",
"manufacturer_data_start": [
0,
],
"manufacturer_id": 27,
"service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb",
},
{
"connectable": False,
"domain": "thermobeacon",
+1 -1
View File
@@ -22,7 +22,7 @@ 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
home-assistant-frontend==20221230.0
httpx==0.23.1
ifaddr==0.1.7
janus==1.0.0
+85
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."""
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.1.0.dev0"
version = "2023.1.0b4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
+17 -17
View File
@@ -40,7 +40,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.33.0
PySwitchbot==0.36.1
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -177,7 +177,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==2.4.2
aiohomekit==2.4.3
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -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
@@ -339,7 +339,7 @@ anthemav==1.4.1
apcaccess==0.0.13
# homeassistant.components.apprise
apprise==1.2.0
apprise==1.2.1
# homeassistant.components.aprs
aprslib==0.7.0
@@ -741,7 +741,7 @@ 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
@@ -888,7 +888,7 @@ hole==0.8.0
holidays==0.17.2
# homeassistant.components.frontend
home-assistant-frontend==20221228.0
home-assistant-frontend==20221230.0
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -1110,7 +1110,7 @@ mill-local==0.2.0
millheater==0.10.0
# homeassistant.components.minio
minio==5.0.10
minio==7.1.12
# homeassistant.components.moat
moat-ble==0.1.1
@@ -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.14
# homeassistant.components.motioneye
motioneye-client==0.3.12
@@ -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.17
# homeassistant.components.edimax
pyedimax==0.2.1
@@ -1674,7 +1674,7 @@ pyirishrail==0.0.2
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.9
pyisy==3.0.10
# homeassistant.components.itach
pyitachip2ir==0.0.7
@@ -1767,7 +1767,7 @@ pymyq==3.1.4
pymysensors==0.24.0
# homeassistant.components.netgear
pynetgear==0.10.8
pynetgear==0.10.9
# homeassistant.components.netio
pynetio==0.1.9.1
@@ -2038,7 +2038,7 @@ python-kasa==0.5.0
# python-lirc==1.2.3
# homeassistant.components.matter
python-matter-server==1.0.7
python-matter-server==1.0.8
# homeassistant.components.xiaomi_miio
python-miio==0.5.12
@@ -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
@@ -2435,7 +2435,7 @@ tesla-wall-connector==1.0.2
# tf-models-official==2.5.0
# homeassistant.components.thermobeacon
thermobeacon-ble==0.4.0
thermobeacon-ble==0.6.0
# homeassistant.components.thermopro
thermopro-ble==0.4.3
+16 -16
View File
@@ -36,7 +36,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.33.0
PySwitchbot==0.36.1
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -161,7 +161,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==2.4.2
aiohomekit==2.4.3
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -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
@@ -305,7 +305,7 @@ anthemav==1.4.1
apcaccess==0.0.13
# homeassistant.components.apprise
apprise==1.2.0
apprise==1.2.1
# homeassistant.components.aprs
aprslib==0.7.0
@@ -557,7 +557,7 @@ 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
@@ -668,7 +668,7 @@ hole==0.8.0
holidays==0.17.2
# homeassistant.components.frontend
home-assistant-frontend==20221228.0
home-assistant-frontend==20221230.0
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -812,7 +812,7 @@ mill-local==0.2.0
millheater==0.10.0
# homeassistant.components.minio
minio==5.0.10
minio==7.1.12
# homeassistant.components.moat
moat-ble==0.1.1
@@ -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.14
# homeassistant.components.motioneye
motioneye-client==0.3.12
@@ -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.17
# homeassistant.components.efergy
pyefergy==22.1.1
@@ -1187,7 +1187,7 @@ pyiqvia==2022.04.0
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.9
pyisy==3.0.10
# homeassistant.components.kaleidescape
pykaleidescape==1.0.1
@@ -1256,7 +1256,7 @@ pymyq==3.1.4
pymysensors==0.24.0
# homeassistant.components.netgear
pynetgear==0.10.8
pynetgear==0.10.9
# homeassistant.components.nina
pynina==0.2.0
@@ -1428,7 +1428,7 @@ python-juicenet==1.1.0
python-kasa==0.5.0
# homeassistant.components.matter
python-matter-server==1.0.7
python-matter-server==1.0.8
# homeassistant.components.xiaomi_miio
python-miio==0.5.12
@@ -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
@@ -1693,7 +1693,7 @@ tesla-powerwall==0.3.18
tesla-wall-connector==1.0.2
# homeassistant.components.thermobeacon
thermobeacon-ble==0.4.0
thermobeacon-ble==0.6.0
# homeassistant.components.thermopro
thermopro-ble==0.4.3
+17 -7
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()
+29 -30
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
+2 -8
View File
@@ -12,9 +12,9 @@ from tests.common import MockConfigEntry, load_fixture
@pytest.fixture(name="config_entry")
def config_entry_fixture(hass, config, unique_id):
def config_entry_fixture(hass, config):
"""Define a config entry fixture."""
entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config)
entry = MockConfigEntry(domain=DOMAIN, unique_id="XXXXXXX", data=config)
entry.add_to_hass(hass)
return entry
@@ -69,9 +69,3 @@ async def setup_airvisual_pro_fixture(hass, config, pro):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
yield
@pytest.fixture(name="unique_id")
def unique_id_fixture(hass):
"""Define a config entry unique ID fixture."""
return "192.168.1.101"
@@ -52,10 +52,16 @@ async def test_create_entry(
}
async def test_duplicate_error(hass, config, config_entry):
async def test_duplicate_error(hass, config, config_entry, setup_airvisual_pro):
"""Test that errors are shown when duplicates are added."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=config
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=config
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "already_configured"
@@ -17,7 +17,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisua
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"source": "user",
"unique_id": "192.168.1.101",
"unique_id": "XXXXXXX",
"disabled_by": None,
},
"data": {
+79 -140
View File
@@ -124,7 +124,14 @@ async def test_ssdp_discovery(hass):
assert result["step_id"] == "authorize"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False}
result["flow_id"], user_input={CONF_USE_PSK: False}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "pin"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
@@ -185,68 +192,76 @@ async def test_user_invalid_host(hass):
assert result["errors"] == {CONF_HOST: "invalid_host"}
async def test_authorize_invalid_auth(hass):
"""Test that authorization errors shown on the authorization step."""
@pytest.mark.parametrize(
"side_effect, error_message",
[
(BraviaTVAuthError, "invalid_auth"),
(BraviaTVNotSupported, "unsupported_model"),
(BraviaTVConnectionError, "cannot_connect"),
],
)
async def test_pin_form_error(hass, side_effect, error_message):
"""Test that PIN form errors are correct."""
with patch(
"pybravia.BraviaTV.connect",
side_effect=BraviaTVAuthError,
side_effect=side_effect,
), patch("pybravia.BraviaTV.pair"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PSK: False}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"}
)
assert result["errors"] == {"base": "invalid_auth"}
assert result["errors"] == {"base": error_message}
async def test_authorize_cannot_connect(hass):
"""Test that errors are shown when cannot connect to host at the authorize step."""
@pytest.mark.parametrize(
"side_effect, error_message",
[
(BraviaTVAuthError, "invalid_auth"),
(BraviaTVNotSupported, "unsupported_model"),
(BraviaTVConnectionError, "cannot_connect"),
],
)
async def test_psk_form_error(hass, side_effect, error_message):
"""Test that PSK form errors are correct."""
with patch(
"pybravia.BraviaTV.connect",
side_effect=BraviaTVConnectionError,
), patch("pybravia.BraviaTV.pair"):
side_effect=side_effect,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"}
result["flow_id"], user_input={CONF_USE_PSK: True}
)
assert result["errors"] == {"base": "cannot_connect"}
async def test_authorize_model_unsupported(hass):
"""Test that errors are shown when the TV is not supported at the authorize step."""
with patch(
"pybravia.BraviaTV.connect",
side_effect=BraviaTVNotSupported,
), patch("pybravia.BraviaTV.pair"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"}
result["flow_id"], user_input={CONF_PIN: "mypsk"}
)
assert result["errors"] == {"base": "unsupported_model"}
assert result["errors"] == {"base": error_message}
async def test_authorize_no_ip_control(hass):
"""Test that errors are shown when IP Control is disabled on the TV."""
async def test_no_ip_control(hass):
"""Test that error are shown when IP Control is disabled on the TV."""
with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PSK: False}
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "no_ip_control"
async def test_duplicate_error(hass):
"""Test that errors are shown when duplicates are added."""
"""Test that error are shown when duplicates are added."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="very_unique_string",
@@ -268,6 +283,9 @@ async def test_duplicate_error(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PSK: False}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"}
)
@@ -277,7 +295,7 @@ async def test_duplicate_error(hass):
async def test_create_entry(hass):
"""Test that the user step works."""
"""Test that entry is added correctly with PIN auth."""
uuid = await instance_id.async_get(hass)
with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch(
@@ -296,7 +314,14 @@ async def test_create_entry(hass):
assert result["step_id"] == "authorize"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False}
result["flow_id"], user_input={CONF_USE_PSK: False}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "pin"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
@@ -312,47 +337,9 @@ async def test_create_entry(hass):
}
async def test_create_entry_with_ipv6_address(hass):
"""Test that the user step works with device IPv6 address."""
uuid = await instance_id.async_get(hass)
with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch(
"pybravia.BraviaTV.set_wol_mode"
), patch(
"pybravia.BraviaTV.get_system_info",
return_value=BRAVIA_SYSTEM_INFO,
), patch(
"homeassistant.components.braviatv.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={CONF_HOST: "2001:db8::1428:57ab"},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "authorize"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["result"].unique_id == "very_unique_string"
assert result["title"] == "TV-Model"
assert result["data"] == {
CONF_HOST: "2001:db8::1428:57ab",
CONF_PIN: "1234",
CONF_USE_PSK: False,
CONF_MAC: "AA:BB:CC:DD:EE:FF",
CONF_CLIENT_ID: uuid,
CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}",
}
async def test_create_entry_psk(hass):
"""Test that the user step works with PSK auth."""
with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch(
"""Test that entry is added correctly with PSK auth."""
with patch("pybravia.BraviaTV.connect"), patch(
"pybravia.BraviaTV.set_wol_mode"
), patch(
"pybravia.BraviaTV.get_system_info",
@@ -368,7 +355,14 @@ async def test_create_entry_psk(hass):
assert result["step_id"] == "authorize"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True}
result["flow_id"], user_input={CONF_USE_PSK: True}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "psk"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "mypsk"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
@@ -474,11 +468,14 @@ async def test_options_flow_error(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
"user_input",
[{CONF_PIN: "mypsk", CONF_USE_PSK: True}, {CONF_PIN: "1234", CONF_USE_PSK: False}],
"use_psk, new_pin",
[
(True, "7777"),
(False, "newpsk"),
],
)
async def test_reauth_successful(hass, user_input):
"""Test starting a reauthentication flow."""
async def test_reauth_successful(hass, use_psk, new_pin):
"""Test that the reauthorization is successful."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="very_unique_string",
@@ -508,73 +505,15 @@ async def test_reauth_successful(hass, user_input):
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["step_id"] == "authorize"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=user_input,
result["flow_id"], user_input={CONF_USE_PSK: use_psk}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: new_pin}
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
async def test_reauth_unsuccessful(hass):
"""Test reauthentication flow failed."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="very_unique_string",
data={
CONF_HOST: "bravia-host",
CONF_PIN: "1234",
CONF_MAC: "AA:BB:CC:DD:EE:FF",
},
title="TV-Model",
)
config_entry.add_to_hass(hass)
with patch(
"pybravia.BraviaTV.connect",
side_effect=BraviaTVAuthError,
), patch("pybravia.BraviaTV.pair"):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id},
data=config_entry.data,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True},
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "reauth_unsuccessful"
async def test_reauth_unsuccessful_during_pairing(hass):
"""Test reauthentication flow failed because of pairing error."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="very_unique_string",
data={
CONF_HOST: "bravia-host",
CONF_PIN: "1234",
CONF_MAC: "AA:BB:CC:DD:EE:FF",
},
title="TV-Model",
)
config_entry.add_to_hass(hass)
with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id},
data=config_entry.data,
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "reauth_unsuccessful"
assert config_entry.data[CONF_PIN] == new_pin
+42 -1
View File
@@ -214,6 +214,47 @@ DESCRIPTION:The bell tolls for thee
RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12
END:VEVENT
END:VCALENDAR
""",
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:14
DTSTAMP:20151125T000000Z
DTSTART:20151127T000000Z
DTEND:20151127T003000Z
RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12
END:VEVENT
END:VCALENDAR
""",
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
TZID:Europe/London
BEGIN:STANDARD
DTSTART:19961027T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
TZNAME:GMT
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19810329T010000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
TZNAME:BST
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:15
DTSTAMP:20221125T000000Z
DTSTART;TZID=Europe/London:20221127T000000
DTEND;TZID=Europe/London:20221127T003000
SUMMARY:Event with a provided Timezone
END:VEVENT
END:VCALENDAR
""",
]
@@ -917,7 +958,7 @@ async def test_get_events(hass, calendar, get_api_events):
await hass.async_block_till_done()
events = await get_api_events("calendar.private")
assert len(events) == 14
assert len(events) == 16
assert calendar.call
@@ -270,7 +270,23 @@ async def test_get_trigger_capabilities_hvac_mode(hass):
assert voluptuous_serialize.convert(
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}]
) == [
{
"name": "to",
"options": [
("off", "off"),
("heat", "heat"),
("cool", "cool"),
("heat_cool", "heat_cool"),
("auto", "auto"),
("dry", "dry"),
("fan_only", "fan_only"),
],
"required": True,
"type": "select",
},
{"name": "for", "optional": True, "type": "positive_time_period_dict"},
]
@pytest.mark.parametrize(
+11 -7
View File
@@ -23,8 +23,10 @@ from homeassistant.const import (
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
ENERGY_KILO_WATT_HOUR,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
VOLUME_CUBIC_METERS,
UnitOfPower,
)
from homeassistant.helpers import entity_registry as er
@@ -56,13 +58,13 @@ async def test_default_setup(hass, dsmr_connection_fixture):
telegram = {
CURRENT_ELECTRICITY_USAGE: CosemObject(
[{"value": Decimal("0.0"), "unit": ENERGY_KILO_WATT_HOUR}]
[{"value": Decimal("0.0"), "unit": UnitOfPower.WATT}]
),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
GAS_METER_READING: MBusObject(
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
{"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS},
]
),
}
@@ -88,9 +90,9 @@ async def test_default_setup(hass, dsmr_connection_fixture):
telegram_callback = connection_factory.call_args_list[0][0][2]
# make sure entities have been created and return 'unknown' state
# make sure entities have been created and return 'unavailable' state
power_consumption = hass.states.get("sensor.electricity_meter_power_consumption")
assert power_consumption.state == STATE_UNKNOWN
assert power_consumption.state == STATE_UNAVAILABLE
assert (
power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
)
@@ -110,9 +112,7 @@ async def test_default_setup(hass, dsmr_connection_fixture):
# ensure entities have new state value after incoming telegram
power_consumption = hass.states.get("sensor.electricity_meter_power_consumption")
assert power_consumption.state == "0.0"
assert (
power_consumption.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR
)
assert power_consumption.attributes.get("unit_of_measurement") == UnitOfPower.WATT
# tariff should be translated in human readable and have no unit
active_tariff = hass.states.get("sensor.electricity_meter_active_tariff")
@@ -784,6 +784,10 @@ async def test_reconnect(hass, dsmr_connection_fixture):
assert connection_factory.call_count == 1
state = hass.states.get("sensor.electricity_meter_power_consumption")
assert state
assert state.state == STATE_UNKNOWN
# indicate disconnect, release wait lock and allow reconnect to happen
closed.set()
# wait for lock set to resolve
+19 -3
View File
@@ -14,7 +14,7 @@ from aiohttp.client_exceptions import ClientError
from gcal_sync.auth import API_BASE_URL
import pytest
from homeassistant.components.google.const import DOMAIN
from homeassistant.components.google.const import CONF_CALENDAR_ACCESS, DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -1054,8 +1054,24 @@ async def test_websocket_delete_recurring_event_instance(
@pytest.mark.parametrize(
"calendar_access_role",
["reader"],
"calendar_access_role,token_scopes,config_entry_options",
[
(
"reader",
["https://www.googleapis.com/auth/calendar"],
{CONF_CALENDAR_ACCESS: "read_write"},
),
(
"reader",
["https://www.googleapis.com/auth/calendar.readonly"],
{CONF_CALENDAR_ACCESS: "read_only"},
),
(
"owner",
["https://www.googleapis.com/auth/calendar.readonly"],
{CONF_CALENDAR_ACCESS: "read_only"},
),
],
)
async def test_readonly_websocket_create(
hass: HomeAssistant,
@@ -41,7 +41,8 @@ async def test_broadcast_one_target(
target = "basement"
expected_command = "broadcast to basement time for dinner"
with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist"
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None],
) as mock_assist_call:
await hass.services.async_call(
notify.DOMAIN,
@@ -64,7 +65,8 @@ async def test_broadcast_two_targets(
expected_command1 = "broadcast to basement time for dinner"
expected_command2 = "broadcast to master bedroom time for dinner"
with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist"
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None],
) as mock_assist_call:
await hass.services.async_call(
notify.DOMAIN,
@@ -84,7 +86,8 @@ async def test_broadcast_empty_message(
await setup_integration()
with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist"
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None],
) as mock_assist_call:
await hass.services.async_call(
notify.DOMAIN,
@@ -1,4 +1,6 @@
"""Test homekit_controller diagnostics."""
from unittest.mock import ANY
from aiohttp import ClientSession
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
@@ -247,8 +249,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc
"friendly_name": "Koogeek-LS1-20833F Identify"
},
"entity_id": "button.koogeek_ls1_20833f_identify",
"last_changed": "2023-01-01T00:00:00+00:00",
"last_updated": "2023-01-01T00:00:00+00:00",
"last_changed": ANY,
"last_updated": ANY,
"state": "unknown",
},
"unit_of_measurement": None,
@@ -269,8 +271,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc
"supported_features": 0,
},
"entity_id": "light.koogeek_ls1_20833f_light_strip",
"last_changed": "2023-01-01T00:00:00+00:00",
"last_updated": "2023-01-01T00:00:00+00:00",
"last_changed": ANY,
"last_updated": ANY,
"state": "off",
},
"unit_of_measurement": None,
@@ -518,8 +520,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow):
"friendly_name": "Koogeek-LS1-20833F " "Identify"
},
"entity_id": "button.koogeek_ls1_20833f_identify",
"last_changed": "2023-01-01T00:00:00+00:00",
"last_updated": "2023-01-01T00:00:00+00:00",
"last_changed": ANY,
"last_updated": ANY,
"state": "unknown",
},
"unit_of_measurement": None,
@@ -540,8 +542,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow):
"supported_features": 0,
},
"entity_id": "light.koogeek_ls1_20833f_light_strip",
"last_changed": "2023-01-01T00:00:00+00:00",
"last_updated": "2023-01-01T00:00:00+00:00",
"last_changed": ANY,
"last_updated": ANY,
"state": "off",
},
"unit_of_measurement": None,
+15 -2
View File
@@ -82,6 +82,9 @@ async def test_config_flow_manual_success(hass):
CONF_PORT: TEST_PORT,
const.CONF_USE_HTTPS: TEST_USE_HTTPS,
}
assert result["options"] == {
const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL,
}
async def test_config_flow_errors(hass):
@@ -174,6 +177,9 @@ async def test_config_flow_errors(hass):
CONF_PORT: TEST_PORT,
const.CONF_USE_HTTPS: TEST_USE_HTTPS,
}
assert result["options"] == {
const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL,
}
async def test_options_flow(hass):
@@ -188,6 +194,9 @@ async def test_options_flow(hass):
CONF_PORT: TEST_PORT,
const.CONF_USE_HTTPS: TEST_USE_HTTPS,
},
options={
const.CONF_PROTOCOL: "rtsp",
},
title=TEST_NVR_NAME,
)
config_entry.add_to_hass(hass)
@@ -202,12 +211,12 @@ async def test_options_flow(hass):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={const.CONF_PROTOCOL: "rtsp"},
user_input={const.CONF_PROTOCOL: "rtmp"},
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert config_entry.options == {
const.CONF_PROTOCOL: "rtsp",
const.CONF_PROTOCOL: "rtmp",
}
@@ -223,6 +232,9 @@ async def test_change_connection_settings(hass):
CONF_PORT: TEST_PORT,
const.CONF_USE_HTTPS: TEST_USE_HTTPS,
},
options={
const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL,
},
title=TEST_NVR_NAME,
)
config_entry.add_to_hass(hass)
@@ -245,6 +257,7 @@ async def test_change_connection_settings(hass):
)
assert result["type"] is data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] == TEST_HOST2
assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2
assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2
+1 -1
View File
@@ -1144,7 +1144,7 @@ async def test_device_classes_with_invalid_unit_of_measurement(
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=None,
native_value="1.0",
device_class=device_class,
native_unit_of_measurement="INVALID!",
)
+1 -1
View File
@@ -180,7 +180,7 @@ WOLOCK_SERVICE_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(
local_name="WoLock",
manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\xda\x83\x00 "},
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"o\x80d"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"o\x80d"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoLock"),
+215 -45
View File
@@ -2,13 +2,21 @@
from unittest.mock import patch
from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError
from homeassistant.components.switchbot.const import (
CONF_ENCRYPTION_KEY,
CONF_KEY_ID,
CONF_RETRY_COUNT,
)
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE
from homeassistant.const import (
CONF_ADDRESS,
CONF_NAME,
CONF_PASSWORD,
CONF_SENSOR_TYPE,
CONF_USERNAME,
)
from homeassistant.data_entry_flow import FlowResultType
from . import (
@@ -85,6 +93,66 @@ async def test_bluetooth_discovery_requires_password(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_bluetooth_discovery_lock_key(hass):
"""Test discovery via bluetooth with a lock."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=WOLOCK_SERVICE_INFO,
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "lock_choose_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "lock_key"}
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_key"
assert result["errors"] == {}
with patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
return_value=False,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KEY_ID: "",
CONF_ENCRYPTION_KEY: "",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_key"
assert result["errors"] == {"base": "encryption_key_invalid"}
with patch_async_setup_entry() as mock_setup_entry, patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KEY_ID: "ff",
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Lock EEFF"
assert result["data"] == {
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_KEY_ID: "ff",
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
CONF_SENSOR_TYPE: "lock",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_bluetooth_discovery_already_setup(hass):
"""Test discovery via bluetooth with a valid device when already setup."""
entry = MockConfigEntry(
@@ -327,7 +395,7 @@ async def test_user_setup_single_bot_with_password(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_setup_wolock(hass):
async def test_user_setup_wolock_key(hass):
"""Test the user initiated form for a lock."""
with patch(
@@ -337,14 +405,39 @@ async def test_user_setup_wolock(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "lock_choose_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "lock_key"}
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_key"
assert result["errors"] == {}
with patch_async_setup_entry() as mock_setup_entry, patch(
"switchbot.SwitchbotLock.verify_encryption_key", return_value=True
with patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
return_value=False,
):
result2 = await hass.config_entries.flow.async_configure(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KEY_ID: "",
CONF_ENCRYPTION_KEY: "",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_key"
assert result["errors"] == {"base": "encryption_key_invalid"}
with patch_async_setup_entry() as mock_setup_entry, patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KEY_ID: "ff",
@@ -353,9 +446,9 @@ async def test_user_setup_wolock(hass):
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Lock EEFF"
assert result2["data"] == {
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Lock EEFF"
assert result["data"] == {
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_KEY_ID: "ff",
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
@@ -365,6 +458,111 @@ async def test_user_setup_wolock(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_setup_wolock_auth(hass):
"""Test the user initiated form for a lock."""
with patch(
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
return_value=[WOLOCK_SERVICE_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "lock_choose_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "lock_auth"}
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_auth"
assert result["errors"] == {}
with patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
side_effect=SwitchbotAuthenticationError,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "",
CONF_PASSWORD: "",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_auth"
assert result["errors"] == {"base": "auth_failed"}
with patch_async_setup_entry() as mock_setup_entry, patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
return_value=True,
), patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
return_value={
CONF_KEY_ID: "ff",
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
},
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Lock EEFF"
assert result["data"] == {
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_KEY_ID: "ff",
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
CONF_SENSOR_TYPE: "lock",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_setup_wolock_auth_switchbot_api_down(hass):
"""Test the user initiated form for a lock when the switchbot api is down."""
with patch(
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
return_value=[WOLOCK_SERVICE_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "lock_choose_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "lock_auth"}
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_auth"
assert result["errors"] == {}
with patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
side_effect=SwitchbotAccountConnectionError,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "",
CONF_PASSWORD: "",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_user_setup_wolock_or_bot(hass):
"""Test the user initiated form for a lock."""
@@ -387,12 +585,20 @@ async def test_user_setup_wolock_or_bot(hass):
USER_INPUT,
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "lock_choose_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "lock_key"}
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_key"
assert result["errors"] == {}
with patch_async_setup_entry() as mock_setup_entry, patch(
"switchbot.SwitchbotLock.verify_encryption_key", return_value=True
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -415,42 +621,6 @@ async def test_user_setup_wolock_or_bot(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_setup_wolock_invalid_encryption_key(hass):
"""Test the user initiated form for a lock with invalid encryption key."""
with patch(
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
return_value=[WOLOCK_SERVICE_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_key"
assert result["errors"] == {}
with patch_async_setup_entry() as mock_setup_entry, patch(
"switchbot.SwitchbotLock.verify_encryption_key", return_value=False
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KEY_ID: "",
CONF_ENCRYPTION_KEY: "",
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.FORM
assert result2["step_id"] == "lock_key"
assert result2["errors"] == {
CONF_KEY_ID: "key_id_invalid",
CONF_ENCRYPTION_KEY: "encryption_key_invalid",
}
assert len(mock_setup_entry.mock_calls) == 0
async def test_user_setup_wosensor(hass):
"""Test the user initiated form with password and valid mac."""
with patch(
@@ -8,7 +8,7 @@ from homeassistant.components.tankerkoenig.const import (
CONF_STATIONS,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
@@ -47,18 +47,6 @@ MOCK_OPTIONS_DATA = {
],
}
MOCK_IMPORT_DATA = {
CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx",
CONF_FUEL_TYPES: ["e5"],
CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0},
CONF_RADIUS: 2.0,
CONF_STATIONS: [
"3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8",
"36b4b812-yyyy-yyyy-yyyy-c51735325858",
],
CONF_SHOW_ON_MAP: True,
}
MOCK_NEARVY_STATIONS_OK = {
"ok": True,
"stations": [
@@ -187,37 +175,6 @@ async def test_user_no_stations(hass: HomeAssistant):
assert result["errors"][CONF_RADIUS] == "no_stations"
async def test_import(hass: HomeAssistant):
"""Test starting a flow by import."""
with patch(
"homeassistant.components.tankerkoenig.async_setup_entry", return_value=True
) as mock_setup_entry, patch(
"homeassistant.components.tankerkoenig.config_flow.getNearbyStations",
return_value=MOCK_NEARVY_STATIONS_OK,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"][CONF_NAME] == "Home"
assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx"
assert result["data"][CONF_FUEL_TYPES] == ["e5"]
assert result["data"][CONF_LOCATION] == {"latitude": 51.0, "longitude": 13.0}
assert result["data"][CONF_RADIUS] == 2.0
assert result["data"][CONF_STATIONS] == [
"3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8",
"36b4b812-xxxx-xxxx-xxxx-c51735325858",
"3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8",
"36b4b812-yyyy-yyyy-yyyy-c51735325858",
]
assert result["options"][CONF_SHOW_ON_MAP]
await hass.async_block_till_done()
assert mock_setup_entry.called
async def test_reauth(hass: HomeAssistant):
"""Test starting a flow by user to re-auth."""
+132
View File
@@ -2,7 +2,10 @@
import pytest
from homeassistant.const import (
UnitOfDataRate,
UnitOfElectricCurrent,
UnitOfEnergy,
UnitOfInformation,
UnitOfLength,
UnitOfMass,
UnitOfPower,
@@ -15,8 +18,11 @@ from homeassistant.const import (
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DataRateConverter,
DistanceConverter,
ElectricCurrentConverter,
EnergyConverter,
InformationConverter,
MassConverter,
PowerConverter,
PressureConverter,
@@ -31,6 +37,7 @@ INVALID_SYMBOL = "bob"
@pytest.mark.parametrize(
"converter,valid_unit",
[
(DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND),
(DistanceConverter, UnitOfLength.KILOMETERS),
(DistanceConverter, UnitOfLength.METERS),
(DistanceConverter, UnitOfLength.CENTIMETERS),
@@ -39,10 +46,13 @@ INVALID_SYMBOL = "bob"
(DistanceConverter, UnitOfLength.YARDS),
(DistanceConverter, UnitOfLength.FEET),
(DistanceConverter, UnitOfLength.INCHES),
(ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE),
(ElectricCurrentConverter, UnitOfElectricCurrent.MILLIAMPERE),
(EnergyConverter, UnitOfEnergy.WATT_HOUR),
(EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
(EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR),
(EnergyConverter, UnitOfEnergy.GIGA_JOULE),
(InformationConverter, UnitOfInformation.GIGABYTES),
(MassConverter, UnitOfMass.GRAMS),
(MassConverter, UnitOfMass.KILOGRAMS),
(MassConverter, UnitOfMass.MICROGRAMS),
@@ -85,8 +95,11 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str)
@pytest.mark.parametrize(
"converter,valid_unit",
[
(DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND),
(DistanceConverter, UnitOfLength.KILOMETERS),
(ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE),
(EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
(InformationConverter, UnitOfInformation.GIBIBYTES),
(MassConverter, UnitOfMass.GRAMS),
(PowerConverter, UnitOfPower.WATT),
(PressureConverter, UnitOfPressure.PA),
@@ -111,8 +124,18 @@ def test_convert_invalid_unit(
@pytest.mark.parametrize(
"converter,from_unit,to_unit",
[
(
DataRateConverter,
UnitOfDataRate.BYTES_PER_SECOND,
UnitOfDataRate.BITS_PER_SECOND,
),
(DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS),
(EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR),
(
InformationConverter,
UnitOfInformation.GIBIBYTES,
UnitOfInformation.GIGABYTES,
),
(MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS),
(PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT),
(PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG),
@@ -132,8 +155,21 @@ def test_convert_nonnumeric_value(
@pytest.mark.parametrize(
"converter,from_unit,to_unit,expected",
[
(
DataRateConverter,
UnitOfDataRate.BITS_PER_SECOND,
UnitOfDataRate.BYTES_PER_SECOND,
8,
),
(DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000),
(
ElectricCurrentConverter,
UnitOfElectricCurrent.AMPERE,
UnitOfElectricCurrent.MILLIAMPERE,
1 / 1000,
),
(EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000),
(InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8),
(PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000),
(
PressureConverter,
@@ -168,6 +204,65 @@ def test_get_unit_ratio(
assert converter.get_unit_ratio(from_unit, to_unit) == expected
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
(8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND),
(8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND),
(8e9, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.GIGABITS_PER_SECOND),
(8, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.BYTES_PER_SECOND),
(8e3, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.KILOBYTES_PER_SECOND),
(8e6, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.MEGABYTES_PER_SECOND),
(8e9, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.GIGABYTES_PER_SECOND),
(
8 * 2**10,
UnitOfDataRate.BITS_PER_SECOND,
1,
UnitOfDataRate.KIBIBYTES_PER_SECOND,
),
(
8 * 2**20,
UnitOfDataRate.BITS_PER_SECOND,
1,
UnitOfDataRate.MEBIBYTES_PER_SECOND,
),
(
8 * 2**30,
UnitOfDataRate.BITS_PER_SECOND,
1,
UnitOfDataRate.GIBIBYTES_PER_SECOND,
),
],
)
def test_data_rate_convert(
value: float,
from_unit: str,
expected: float,
to_unit: str,
) -> None:
"""Test conversion to other units."""
assert DataRateConverter.convert(value, from_unit, to_unit) == pytest.approx(
expected
)
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
(5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE),
(5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE),
],
)
def test_electric_current_convert(
value: float,
from_unit: str,
expected: float,
to_unit: str,
) -> None:
"""Test conversion to other units."""
assert ElectricCurrentConverter.convert(value, from_unit, to_unit) == expected
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
@@ -307,6 +402,43 @@ def test_energy_convert(
assert EnergyConverter.convert(value, from_unit, to_unit) == expected
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
(8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS),
(8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS),
(8e9, UnitOfInformation.BITS, 8, UnitOfInformation.GIGABITS),
(8, UnitOfInformation.BITS, 1, UnitOfInformation.BYTES),
(8e3, UnitOfInformation.BITS, 1, UnitOfInformation.KILOBYTES),
(8e6, UnitOfInformation.BITS, 1, UnitOfInformation.MEGABYTES),
(8e9, UnitOfInformation.BITS, 1, UnitOfInformation.GIGABYTES),
(8e12, UnitOfInformation.BITS, 1, UnitOfInformation.TERABYTES),
(8e15, UnitOfInformation.BITS, 1, UnitOfInformation.PETABYTES),
(8e18, UnitOfInformation.BITS, 1, UnitOfInformation.EXABYTES),
(8e21, UnitOfInformation.BITS, 1, UnitOfInformation.ZETTABYTES),
(8e24, UnitOfInformation.BITS, 1, UnitOfInformation.YOTTABYTES),
(8 * 2**10, UnitOfInformation.BITS, 1, UnitOfInformation.KIBIBYTES),
(8 * 2**20, UnitOfInformation.BITS, 1, UnitOfInformation.MEBIBYTES),
(8 * 2**30, UnitOfInformation.BITS, 1, UnitOfInformation.GIBIBYTES),
(8 * 2**40, UnitOfInformation.BITS, 1, UnitOfInformation.TEBIBYTES),
(8 * 2**50, UnitOfInformation.BITS, 1, UnitOfInformation.PEBIBYTES),
(8 * 2**60, UnitOfInformation.BITS, 1, UnitOfInformation.EXBIBYTES),
(8 * 2**70, UnitOfInformation.BITS, 1, UnitOfInformation.ZEBIBYTES),
(8 * 2**80, UnitOfInformation.BITS, 1, UnitOfInformation.YOBIBYTES),
],
)
def test_information_convert(
value: float,
from_unit: str,
expected: float,
to_unit: str,
) -> None:
"""Test conversion to other units."""
assert InformationConverter.convert(value, from_unit, to_unit) == pytest.approx(
expected
)
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[