forked from home-assistant/core
Compare commits
96 Commits
2023.1.0b2
...
2023.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3454bfd9c | ||
|
|
834847988d | ||
|
|
caf15534bb | ||
|
|
10cb2e31c4 | ||
|
|
85c9f9facf | ||
|
|
5ff7b3bb1a | ||
|
|
e5ba423d6d | ||
|
|
b30d4ef7cf | ||
|
|
00e563f1b8 | ||
|
|
cf06f3b81d | ||
|
|
a781fcca86 | ||
|
|
764550f2e1 | ||
|
|
7396bcc585 | ||
|
|
7e6b087773 | ||
|
|
71ce7373a3 | ||
|
|
33bb9c230b | ||
|
|
f0f2c12d91 | ||
|
|
2840821594 | ||
|
|
edfd83c3a7 | ||
|
|
ee88f34a91 | ||
|
|
fa4c250001 | ||
|
|
59d6f827c3 | ||
|
|
26ea02aa8f | ||
|
|
d73b86132b | ||
|
|
8034faadca | ||
|
|
3c2b7c0d69 | ||
|
|
563ad02c65 | ||
|
|
fe89b663e7 | ||
|
|
dcd07d3135 | ||
|
|
8bf2299407 | ||
|
|
9c689d757c | ||
|
|
4e4fc1767f | ||
|
|
cc3c5772c5 | ||
|
|
6ba6991ecd | ||
|
|
d52d068469 | ||
|
|
09b3611a63 | ||
|
|
ab2f05d3e9 | ||
|
|
90ac0c870f | ||
|
|
0fd113db59 | ||
|
|
1b43323f5e | ||
|
|
6108e581b1 | ||
|
|
c8c68f05ec | ||
|
|
b80467dc58 | ||
|
|
6e9f0eca03 | ||
|
|
cc6a2f0338 | ||
|
|
6ebf2ec9ec | ||
|
|
9ecee11af6 | ||
|
|
9a1669103b | ||
|
|
368ea0586d | ||
|
|
4a7db6ee71 | ||
|
|
a10b9572c7 | ||
|
|
0b47bf1f0b | ||
|
|
5f4d286556 | ||
|
|
b23ab3c65a | ||
|
|
7c199b36f8 | ||
|
|
d4e55ee030 | ||
|
|
f3ec82543e | ||
|
|
4013d4c48d | ||
|
|
93ac908776 | ||
|
|
2ad1a53038 | ||
|
|
3ba59fbebe | ||
|
|
f3fab5c1f5 | ||
|
|
2d120cb6ba | ||
|
|
ad782166c7 | ||
|
|
bc9202cf02 | ||
|
|
0d385d3b67 | ||
|
|
76fa24aba1 | ||
|
|
95ae37cd87 | ||
|
|
bc1d22f4ec | ||
|
|
67e1872ab6 | ||
|
|
516c2b0cdb | ||
|
|
60f067b68f | ||
|
|
ff76567061 | ||
|
|
93488cfa0f | ||
|
|
9655619667 | ||
|
|
32736b3336 | ||
|
|
c77b78928e | ||
|
|
a7ba242f1f | ||
|
|
043d58d697 | ||
|
|
6408890543 | ||
|
|
c5f7d7ae85 | ||
|
|
7ab27cd9bf | ||
|
|
9932c0cb91 | ||
|
|
565d4f85c1 | ||
|
|
7be60d4569 | ||
|
|
a50622cbfd | ||
|
|
fb41b024c0 | ||
|
|
80ac4c0269 | ||
|
|
0e0677b690 | ||
|
|
50d9e3efe6 | ||
|
|
ca28006d76 | ||
|
|
ac3711e6ab | ||
|
|
5901964bf6 | ||
|
|
b24c40f2df | ||
|
|
2cb7a80f98 | ||
|
|
f05de2b28c |
@@ -947,8 +947,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/remote/ @home-assistant/core
|
||||
/homeassistant/components/renault/ @epenet
|
||||
/tests/components/renault/ @epenet
|
||||
/homeassistant/components/reolink/ @starkillerOG @JimStar
|
||||
/tests/components/reolink/ @starkillerOG @JimStar
|
||||
/homeassistant/components/reolink/ @starkillerOG
|
||||
/tests/components/reolink/ @starkillerOG
|
||||
/homeassistant/components/repairs/ @home-assistant/core
|
||||
/tests/components/repairs/ @home-assistant/core
|
||||
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "AdGuard Home",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adguard",
|
||||
"requirements": ["adguardhome==0.5.1"],
|
||||
"requirements": ["adguardhome==0.6.1"],
|
||||
"codeowners": ["@frenck"],
|
||||
"iot_class": "local_polling",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -32,6 +32,7 @@ from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
@@ -117,36 +118,6 @@ def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str:
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pro_config_entry_by_ip_address(
|
||||
hass: HomeAssistant, ip_address: str
|
||||
) -> ConfigEntry:
|
||||
"""Get the Pro config entry related to an IP address."""
|
||||
[config_entry] = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO)
|
||||
if entry.data[CONF_IP_ADDRESS] == ip_address
|
||||
]
|
||||
return config_entry
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pro_device_by_config_entry(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dr.DeviceEntry:
|
||||
"""Get the Pro device entry related to a config entry.
|
||||
|
||||
Note that a Pro config entry can only contain a single device.
|
||||
"""
|
||||
device_registry = dr.async_get(hass)
|
||||
[device_entry] = [
|
||||
device_entry
|
||||
for device_entry in device_registry.devices.values()
|
||||
if config_entry.entry_id in device_entry.config_entries
|
||||
]
|
||||
return device_entry
|
||||
|
||||
|
||||
@callback
|
||||
def async_sync_geo_coordinator_update_intervals(
|
||||
hass: HomeAssistant, api_key: str
|
||||
@@ -306,14 +277,31 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
version = 3
|
||||
|
||||
if entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_NODE_PRO:
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
|
||||
# Get the existing Pro device entry before it is removed by the migration:
|
||||
old_device_entry = async_get_pro_device_by_config_entry(hass, entry)
|
||||
# Store the existing Pro device before the migration removes it:
|
||||
old_device_entry = next(
|
||||
entry
|
||||
for entry in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
# Store the existing Pro entity entries (mapped by unique ID) before the
|
||||
# migration removes it:
|
||||
old_entity_entries: dict[str, er.RegistryEntry] = {
|
||||
entry.unique_id: entry
|
||||
for entry in er.async_entries_for_device(
|
||||
entity_registry, old_device_entry.id, include_disabled_entities=True
|
||||
)
|
||||
}
|
||||
|
||||
# Remove this config entry and create a new one under the `airvisual_pro`
|
||||
# domain:
|
||||
new_entry_data = {**entry.data}
|
||||
new_entry_data.pop(CONF_INTEGRATION_TYPE)
|
||||
|
||||
tasks = [
|
||||
hass.config_entries.async_remove(entry.entry_id),
|
||||
hass.config_entries.flow.async_init(
|
||||
@@ -324,18 +312,52 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# After the migration has occurred, grab the new config and device entries
|
||||
# (now under the `airvisual_pro` domain):
|
||||
new_config_entry = next(
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO)
|
||||
if entry.data[CONF_IP_ADDRESS] == ip_address
|
||||
)
|
||||
new_device_entry = next(
|
||||
entry
|
||||
for entry in dr.async_entries_for_config_entry(
|
||||
device_registry, new_config_entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
# Update the new device entry with any customizations from the old one:
|
||||
device_registry.async_update_device(
|
||||
new_device_entry.id,
|
||||
area_id=old_device_entry.area_id,
|
||||
disabled_by=old_device_entry.disabled_by,
|
||||
name_by_user=old_device_entry.name_by_user,
|
||||
)
|
||||
|
||||
# Update the new entity entries with any customizations from the old ones:
|
||||
for new_entity_entry in er.async_entries_for_device(
|
||||
entity_registry, new_device_entry.id, include_disabled_entities=True
|
||||
):
|
||||
if old_entity_entry := old_entity_entries.get(
|
||||
new_entity_entry.unique_id
|
||||
):
|
||||
entity_registry.async_update_entity(
|
||||
new_entity_entry.entity_id,
|
||||
area_id=old_entity_entry.area_id,
|
||||
device_class=old_entity_entry.device_class,
|
||||
disabled_by=old_entity_entry.disabled_by,
|
||||
hidden_by=old_entity_entry.hidden_by,
|
||||
icon=old_entity_entry.icon,
|
||||
name=old_entity_entry.name,
|
||||
new_entity_id=old_entity_entry.entity_id,
|
||||
unit_of_measurement=old_entity_entry.unit_of_measurement,
|
||||
)
|
||||
|
||||
# If any automations are using the old device ID, create a Repairs issues
|
||||
# with instructions on how to update it:
|
||||
if device_automations := automation.automations_with_device(
|
||||
hass, old_device_entry.id
|
||||
):
|
||||
new_config_entry = async_get_pro_config_entry_by_ip_address(
|
||||
hass, ip_address
|
||||
)
|
||||
new_device_entry = async_get_pro_device_by_config_entry(
|
||||
hass, new_config_entry
|
||||
)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
|
||||
return (
|
||||
self.vehicle.vehicle_location.location[0]
|
||||
if self.vehicle.is_vehicle_tracking_enabled
|
||||
and self.vehicle.vehicle_location.location
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
|
||||
return (
|
||||
self.vehicle.vehicle_location.location[1]
|
||||
if self.vehicle.is_vehicle_tracking_enabled
|
||||
and self.vehicle.vehicle_location.location
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.10.4"],
|
||||
"requirements": ["bimmer_connected==0.12.0"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"requirements": ["bthome-ble==2.4.0"],
|
||||
"requirements": ["bthome-ble==2.4.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@Ernst79"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for WebDav Calendar."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -185,8 +185,8 @@ class WebDavCalendarData:
|
||||
event_list.append(
|
||||
CalendarEvent(
|
||||
summary=self.get_attr_value(vevent, "summary") or "",
|
||||
start=vevent.dtstart.value,
|
||||
end=self.get_end_date(vevent),
|
||||
start=self.to_local(vevent.dtstart.value),
|
||||
end=self.to_local(self.get_end_date(vevent)),
|
||||
location=self.get_attr_value(vevent, "location"),
|
||||
description=self.get_attr_value(vevent, "description"),
|
||||
)
|
||||
@@ -269,8 +269,8 @@ class WebDavCalendarData:
|
||||
)
|
||||
self.event = CalendarEvent(
|
||||
summary=summary,
|
||||
start=vevent.dtstart.value,
|
||||
end=self.get_end_date(vevent),
|
||||
start=self.to_local(vevent.dtstart.value),
|
||||
end=self.to_local(self.get_end_date(vevent)),
|
||||
location=self.get_attr_value(vevent, "location"),
|
||||
description=self.get_attr_value(vevent, "description"),
|
||||
)
|
||||
@@ -308,15 +308,23 @@ class WebDavCalendarData:
|
||||
def to_datetime(obj):
|
||||
"""Return a datetime."""
|
||||
if isinstance(obj, datetime):
|
||||
if obj.tzinfo is None:
|
||||
# floating value, not bound to any time zone in particular
|
||||
# represent same time regardless of which time zone is currently being observed
|
||||
return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE)
|
||||
return obj
|
||||
return WebDavCalendarData.to_local(obj)
|
||||
return dt.dt.datetime.combine(obj, dt.dt.time.min).replace(
|
||||
tzinfo=dt.DEFAULT_TIME_ZONE
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_local(obj: datetime | date) -> datetime | date:
|
||||
"""Return a datetime as a local datetime, leaving dates unchanged.
|
||||
|
||||
This handles giving floating times a timezone for comparison
|
||||
with all day events and dropping the custom timezone object
|
||||
used by the caldav client and dateutil so the datetime can be copied.
|
||||
"""
|
||||
if isinstance(obj, datetime):
|
||||
return dt.as_local(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_attr_value(obj, attribute):
|
||||
"""Return the value of the attribute if defined."""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from pycfdns.exceptions import (
|
||||
CloudflareAuthenticationException,
|
||||
CloudflareConnectionException,
|
||||
CloudflareException,
|
||||
CloudflareZoneException,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -47,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
zone_id = await cfupdate.get_zone_id()
|
||||
except CloudflareAuthenticationException as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
except CloudflareConnectionException as error:
|
||||
except (CloudflareConnectionException, CloudflareZoneException) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
async def update_records(now):
|
||||
|
||||
@@ -75,7 +75,6 @@ async def async_setup(hass):
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/device_registry/update",
|
||||
vol.Optional("aliases"): list,
|
||||
vol.Optional("area_id"): vol.Any(str, None),
|
||||
vol.Required("device_id"): str,
|
||||
# We only allow setting disabled_by user via API.
|
||||
@@ -96,10 +95,6 @@ def websocket_update_device(
|
||||
msg.pop("type")
|
||||
msg_id = msg.pop("id")
|
||||
|
||||
if "aliases" in msg:
|
||||
# Convert aliases to a set
|
||||
msg["aliases"] = set(msg["aliases"])
|
||||
|
||||
if msg.get("disabled_by") is not None:
|
||||
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
|
||||
|
||||
@@ -165,7 +160,6 @@ async def websocket_remove_config_entry_from_device(
|
||||
def _entry_dict(entry):
|
||||
"""Convert entry to API format."""
|
||||
return {
|
||||
"aliases": entry.aliases,
|
||||
"area_id": entry.area_id,
|
||||
"configuration_url": entry.configuration_url,
|
||||
"config_entries": list(entry.config_entries),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,7 +21,6 @@ from .devolo_device import DevoloDeviceEntity
|
||||
DEVICE_CLASS_MAPPING = {
|
||||
"battery": SensorDeviceClass.BATTERY,
|
||||
"temperature": SensorDeviceClass.TEMPERATURE,
|
||||
"light": SensorDeviceClass.ILLUMINANCE,
|
||||
"humidity": SensorDeviceClass.HUMIDITY,
|
||||
"current": SensorDeviceClass.POWER,
|
||||
"total": SensorDeviceClass.ENERGY,
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
UnitOfEnergy,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||
@@ -401,7 +402,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
@Throttle(min_time_between_updates)
|
||||
def update_entities_telegram(telegram: dict[str, DSMRObject]) -> None:
|
||||
def update_entities_telegram(telegram: dict[str, DSMRObject] | None) -> None:
|
||||
"""Update entities with latest telegram and trigger state update."""
|
||||
# Make all device entities aware of new telegram
|
||||
for entity in entities:
|
||||
@@ -445,6 +446,11 @@ async def async_setup_entry(
|
||||
|
||||
while hass.state == CoreState.not_running or hass.is_running:
|
||||
# Start DSMR asyncio.Protocol reader
|
||||
|
||||
# Reflect connected state in devices state by setting an
|
||||
# empty telegram resulting in `unknown` states
|
||||
update_entities_telegram({})
|
||||
|
||||
try:
|
||||
transport, protocol = await hass.loop.create_task(reader_factory())
|
||||
|
||||
@@ -472,8 +478,8 @@ async def async_setup_entry(
|
||||
protocol = None
|
||||
|
||||
# Reflect disconnect state in devices state by setting an
|
||||
# empty telegram resulting in `unknown` states
|
||||
update_entities_telegram({})
|
||||
# None telegram resulting in `unavailable` states
|
||||
update_entities_telegram(None)
|
||||
|
||||
# throttle reconnect attempts
|
||||
await asyncio.sleep(
|
||||
@@ -487,11 +493,19 @@ async def async_setup_entry(
|
||||
transport = None
|
||||
protocol = None
|
||||
|
||||
# Reflect disconnect state in devices state by setting an
|
||||
# None telegram resulting in `unavailable` states
|
||||
update_entities_telegram(None)
|
||||
|
||||
# throttle reconnect attempts
|
||||
await asyncio.sleep(
|
||||
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
|
||||
)
|
||||
except CancelledError:
|
||||
# Reflect disconnect state in devices state by setting an
|
||||
# None telegram resulting in `unavailable` states
|
||||
update_entities_telegram(None)
|
||||
|
||||
if stop_listener and (
|
||||
hass.state == CoreState.not_running or hass.is_running
|
||||
):
|
||||
@@ -534,7 +548,7 @@ class DSMREntity(SensorEntity):
|
||||
"""Initialize entity."""
|
||||
self.entity_description = entity_description
|
||||
self._entry = entry
|
||||
self.telegram: dict[str, DSMRObject] = {}
|
||||
self.telegram: dict[str, DSMRObject] | None = {}
|
||||
|
||||
device_serial = entry.data[CONF_SERIAL_ID]
|
||||
device_name = DEVICE_NAME_ELECTRICITY
|
||||
@@ -551,16 +565,21 @@ class DSMREntity(SensorEntity):
|
||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||
|
||||
@callback
|
||||
def update_data(self, telegram: dict[str, DSMRObject]) -> None:
|
||||
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
|
||||
"""Update data."""
|
||||
self.telegram = telegram
|
||||
if self.hass and self.entity_description.obis_reference in self.telegram:
|
||||
if self.hass and (
|
||||
telegram is None or self.entity_description.obis_reference in telegram
|
||||
):
|
||||
self.async_write_ha_state()
|
||||
|
||||
def get_dsmr_object_attr(self, attribute: str) -> str | None:
|
||||
"""Read attribute from last received telegram for this DSMR object."""
|
||||
# Make sure telegram contains an object for this entities obis
|
||||
if self.entity_description.obis_reference not in self.telegram:
|
||||
if (
|
||||
self.telegram is None
|
||||
or self.entity_description.obis_reference not in self.telegram
|
||||
):
|
||||
return None
|
||||
|
||||
# Get the attribute value if the object has it
|
||||
@@ -568,6 +587,26 @@ class DSMREntity(SensorEntity):
|
||||
attr: str | None = getattr(dsmr_object, attribute)
|
||||
return attr
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Entity is only available if there is a telegram."""
|
||||
return self.telegram is not None
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
"""Return the device class of this entity."""
|
||||
device_class = super().device_class
|
||||
|
||||
# Override device class for gas sensors providing energy units, like
|
||||
# kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas
|
||||
with suppress(ValueError):
|
||||
if device_class == SensorDeviceClass.GAS and UnitOfEnergy(
|
||||
str(self.native_unit_of_measurement)
|
||||
):
|
||||
return SensorDeviceClass.ENERGY
|
||||
|
||||
return device_class
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of sensor, if available, translate if needed."""
|
||||
|
||||
@@ -560,8 +560,8 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/consumption/quarter-hour-peak-electricity/average_delivered",
|
||||
name="Previous quarter-hour peak usage",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/consumption/quarter-hour-peak-electricity/read_at_start",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Rheem EcoNet Products",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"requirements": ["pyeconet==0.1.15"],
|
||||
"requirements": ["pyeconet==0.1.18"],
|
||||
"codeowners": ["@vangorra", "@w1ll1am23"],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"]
|
||||
|
||||
@@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = {
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
}
|
||||
VALID_ENERGY_UNITS: set[str] = {
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
}
|
||||
VALID_ENERGY_UNITS_GAS = {
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
*VALID_ENERGY_UNITS,
|
||||
}
|
||||
VALID_VOLUME_UNITS_WATER: set[str] = {
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
UnitOfVolume.GALLONS,
|
||||
UnitOfVolume.LITERS,
|
||||
|
||||
@@ -22,10 +22,10 @@ from .const import DOMAIN
|
||||
ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
|
||||
ENERGY_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.ENERGY: (
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
)
|
||||
}
|
||||
ENERGY_PRICE_UNITS = tuple(
|
||||
@@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = (
|
||||
)
|
||||
GAS_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.ENERGY: (
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
),
|
||||
sensor.SensorDeviceClass.GAS: (
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
|
||||
}
|
||||
GAS_PRICE_UNITS = tuple(
|
||||
f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units
|
||||
@@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price"
|
||||
WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,)
|
||||
WATER_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.WATER: (
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
UnitOfVolume.GALLONS,
|
||||
UnitOfVolume.LITERS,
|
||||
),
|
||||
|
||||
@@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription(
|
||||
key="aqhi",
|
||||
name="AQHI",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=_get_aqhi_value,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
"after_dependencies": ["bluetooth", "zeroconf", "tag"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"after_dependencies": ["zeroconf", "tag"],
|
||||
"iot_class": "local_push",
|
||||
"integration_type": "device",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20221228.0"],
|
||||
"requirements": ["home-assistant-frontend==20230104.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
for target in call.data[ATTR_DEVICE_ID]:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
coordinator = hass.data[DOMAIN][list(device.config_entries)[0]]
|
||||
# fully_method(coordinator.fully, *args, **kwargs) would make
|
||||
# test_services.py fail.
|
||||
await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs)
|
||||
for key in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(key)
|
||||
if not entry:
|
||||
continue
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
coordinator = hass.data[DOMAIN][key]
|
||||
# fully_method(coordinator.fully, *args, **kwargs) would make
|
||||
# test_services.py fail.
|
||||
await getattr(coordinator.fully, fully_method.__name__)(
|
||||
*args, **kwargs
|
||||
)
|
||||
break
|
||||
|
||||
async def async_load_url(call: ServiceCall) -> None:
|
||||
"""Load a URL on the Fully Kiosk Browser."""
|
||||
|
||||
@@ -221,8 +221,7 @@ async def async_setup_entry(
|
||||
)
|
||||
if (
|
||||
search := data.get(CONF_SEARCH)
|
||||
or calendar_item.access_role == AccessRole.FREE_BUSY_READER
|
||||
):
|
||||
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
|
||||
coordinator = CalendarQueryUpdateCoordinator(
|
||||
hass,
|
||||
calendar_service,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==4.1.0", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==4.1.1", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -70,6 +70,7 @@ def api_error(
|
||||
class AddonInfo:
|
||||
"""Represent the current add-on info state."""
|
||||
|
||||
available: bool
|
||||
hostname: str | None
|
||||
options: dict[str, Any]
|
||||
state: AddonState
|
||||
@@ -144,6 +145,7 @@ class AddonManager:
|
||||
self._logger.debug("Add-on store info: %s", addon_store_info)
|
||||
if not addon_store_info["installed"]:
|
||||
return AddonInfo(
|
||||
available=addon_store_info["available"],
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_INSTALLED,
|
||||
@@ -154,6 +156,7 @@ class AddonManager:
|
||||
addon_info = await async_get_addon_info(self._hass, self.addon_slug)
|
||||
addon_state = self.async_get_addon_state(addon_info)
|
||||
return AddonInfo(
|
||||
available=addon_info["available"],
|
||||
hostname=addon_info["hostname"],
|
||||
options=addon_info["options"],
|
||||
state=addon_state,
|
||||
@@ -184,6 +187,11 @@ class AddonManager:
|
||||
@api_error("Failed to install the {addon_name} add-on")
|
||||
async def async_install_addon(self) -> None:
|
||||
"""Install the managed add-on."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
|
||||
if not addon_info.available:
|
||||
raise AddonError(f"{self.addon_name} add-on is not available anymore")
|
||||
|
||||
await async_install_addon(self._hass, self.addon_slug)
|
||||
|
||||
@api_error("Failed to uninstall the {addon_name} add-on")
|
||||
@@ -196,6 +204,9 @@ class AddonManager:
|
||||
"""Update the managed add-on if needed."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
|
||||
if not addon_info.available:
|
||||
raise AddonError(f"{self.addon_name} add-on is not available anymore")
|
||||
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
raise AddonError(f"{self.addon_name} add-on is not installed")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ DATA_HYDRAWISE = "hydrawise"
|
||||
DOMAIN = "hydrawise"
|
||||
DEFAULT_WATERING_TIME = 15
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""DataUpdateCoordinator for LaCrosse View."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from time import time
|
||||
|
||||
from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor
|
||||
|
||||
@@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
) -> None:
|
||||
"""Initialize DataUpdateCoordinator for LaCrosse View."""
|
||||
self.api = api
|
||||
self.last_update = datetime.utcnow()
|
||||
self.last_update = time()
|
||||
self.username = entry.data["username"]
|
||||
self.password = entry.data["password"]
|
||||
self.hass = hass
|
||||
@@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
|
||||
async def _async_update_data(self) -> list[Sensor]:
|
||||
"""Get the data for LaCrosse View."""
|
||||
now = datetime.utcnow()
|
||||
now = int(time())
|
||||
|
||||
if self.last_update < now - timedelta(minutes=59): # Get new token
|
||||
if self.last_update < now - 59 * 60: # Get new token once in a hour
|
||||
self.last_update = now
|
||||
try:
|
||||
await self.api.login(self.username, self.password)
|
||||
except LoginError as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
# Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request)
|
||||
yesterday = now - timedelta(days=1)
|
||||
yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0)
|
||||
yesterday_timestamp = datetime.timestamp(yesterday)
|
||||
|
||||
try:
|
||||
# Fetch last hour of data
|
||||
sensors = await self.api.get_sensors(
|
||||
location=Location(id=self.id, name=self.name),
|
||||
tz=self.hass.config.time_zone,
|
||||
start=str(int(yesterday_timestamp)),
|
||||
end=str(int(datetime.timestamp(now))),
|
||||
start=str(now - 3600),
|
||||
end=str(now),
|
||||
)
|
||||
except HTTPError as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LCN",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"requirements": ["pypck==0.7.15"],
|
||||
"requirements": ["pypck==0.7.16"],
|
||||
"codeowners": ["@alengwenus"],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"]
|
||||
|
||||
@@ -194,7 +194,7 @@ class VarAbs(LcnServiceCall):
|
||||
vol.Required(CONF_VARIABLE): vol.All(
|
||||
vol.Upper, vol.In(VARIABLES + SETPOINTS)
|
||||
),
|
||||
vol.Optional(CONF_VALUE, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_VALUE, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
|
||||
vol.Upper, vol.In(VAR_UNITS)
|
||||
),
|
||||
@@ -234,7 +234,7 @@ class VarRel(LcnServiceCall):
|
||||
vol.Required(CONF_VARIABLE): vol.All(
|
||||
vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS)
|
||||
),
|
||||
vol.Optional(CONF_VALUE, default=0): int,
|
||||
vol.Optional(CONF_VALUE, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
|
||||
vol.Upper, vol.In(VAR_UNITS)
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/life360",
|
||||
"codeowners": ["@pnbruckner"],
|
||||
"requirements": ["life360==5.3.0"],
|
||||
"requirements": ["life360==5.5.0"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["life360"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Local Calendar",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"requirements": ["ical==4.2.8"],
|
||||
"requirements": ["ical==4.2.9"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"]
|
||||
|
||||
@@ -352,6 +352,13 @@ class MotionTiltDevice(MotionPositionDevice):
|
||||
return None
|
||||
return self._blind.angle * 100 / 180
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Return if the cover is closed or not."""
|
||||
if self._blind.position is None:
|
||||
return None
|
||||
return self._blind.position >= 95
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover tilt."""
|
||||
async with self._api_lock:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Motion Blinds",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"requirements": ["motionblinds==0.6.13"],
|
||||
"requirements": ["motionblinds==0.6.15"],
|
||||
"dependencies": ["network"],
|
||||
"dhcp": [
|
||||
{ "registered_devices": true },
|
||||
|
||||
@@ -59,7 +59,7 @@ CONF_EXPIRE_AFTER = "expire_after"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
|
||||
@@ -6,7 +6,7 @@ import functools
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import button
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.components.button import DEVICE_CLASSES_SCHEMA, ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -39,7 +39,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
|
||||
@@ -161,7 +161,7 @@ def validate_options(config: ConfigType) -> ConfigType:
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
|
||||
@@ -87,7 +87,7 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode),
|
||||
|
||||
@@ -115,6 +115,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||
discovery_data: DiscoveryInfoType | None,
|
||||
) -> None:
|
||||
"""Initialize the MQTT select."""
|
||||
self._attr_current_option = None
|
||||
SelectEntity.__init__(self)
|
||||
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
|
||||
|
||||
@@ -125,7 +126,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||
|
||||
def _setup_from_config(self, config: ConfigType) -> None:
|
||||
"""(Re)Setup the entity."""
|
||||
self._attr_current_option = None
|
||||
self._optimistic = config[CONF_OPTIMISTIC]
|
||||
self._attr_options = config[CONF_OPTIONS]
|
||||
|
||||
|
||||
@@ -98,13 +98,13 @@ def validate_options(conf: ConfigType) -> ConfigType:
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_LAST_RESET_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@@ -62,7 +62,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
|
||||
vol.Optional(CONF_STATE_OFF): cv.string,
|
||||
vol.Optional(CONF_STATE_ON): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ CONF_TITLE = "title"
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_ENTITY_PICTURE): cv.string,
|
||||
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Netatmo",
|
||||
"integration_type": "hub",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
||||
"requirements": ["pyatmo==7.4.0"],
|
||||
"requirements": ["pyatmo==7.5.0"],
|
||||
"after_dependencies": ["cloud", "media_source"],
|
||||
"dependencies": ["application_credentials", "webhook"],
|
||||
"codeowners": ["@cgtobi"],
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -91,7 +91,7 @@ class NumberDeviceClass(StrEnum):
|
||||
CURRENT = "current"
|
||||
"""Current.
|
||||
|
||||
Unit of measurement: `A`
|
||||
Unit of measurement: `A`, `mA`
|
||||
"""
|
||||
|
||||
DATA_RATE = "data_rate"
|
||||
@@ -213,7 +213,7 @@ class NumberDeviceClass(StrEnum):
|
||||
POWER_FACTOR = "power_factor"
|
||||
"""Power factor.
|
||||
|
||||
Unit of measurement: `%`
|
||||
Unit of measurement: `%`, `None`
|
||||
"""
|
||||
|
||||
POWER = "power"
|
||||
@@ -296,7 +296,7 @@ class NumberDeviceClass(StrEnum):
|
||||
VOLTAGE = "voltage"
|
||||
"""Voltage.
|
||||
|
||||
Unit of measurement: `V`
|
||||
Unit of measurement: `V`, `mV`
|
||||
"""
|
||||
|
||||
VOLUME = "volume"
|
||||
|
||||
@@ -210,7 +210,7 @@ class PhilipsTVMediaPlayer(
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send pause command to media player."""
|
||||
if self._tv.quirk_playpause_spacebar:
|
||||
await self._tv.sendUnicode(" ")
|
||||
await self._tv.sendKey("Confirm")
|
||||
else:
|
||||
await self._tv.sendKey("PlayPause")
|
||||
await self._async_update_soon()
|
||||
@@ -509,6 +509,8 @@ class PhilipsTVMediaPlayer(
|
||||
self._media_title = self._sources.get(self._tv.source_id)
|
||||
self._media_channel = None
|
||||
|
||||
self._attr_assumed_state = True
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
|
||||
@@ -6,12 +6,10 @@ from aiopurpleair.models.sensors import SensorModel
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .config_flow import async_remove_sensor_by_device_id
|
||||
from .const import CONF_LAST_UPDATE_SENSOR_ADD, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PurpleAirDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@@ -32,26 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_handle_entry_update(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
if entry.options.get(CONF_LAST_UPDATE_SENSOR_ADD) is True:
|
||||
# If the last options update was to add a sensor, we reload the config entry:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
new_entry_options = async_remove_sensor_by_device_id(
|
||||
hass,
|
||||
config_entry,
|
||||
device_entry.id,
|
||||
# remove_device is set to False because in this instance, the device has
|
||||
# already been removed:
|
||||
remove_device=False,
|
||||
)
|
||||
return hass.config_entries.async_update_entry(
|
||||
config_entry, options=new_entry_options
|
||||
)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow for PurpleAir integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
@@ -14,13 +15,15 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -28,7 +31,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_LAST_UPDATE_SENSOR_ADD, CONF_SENSOR_INDICES, DOMAIN, LOGGER
|
||||
from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER
|
||||
|
||||
CONF_DISTANCE = "distance"
|
||||
CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options"
|
||||
@@ -74,8 +77,7 @@ def async_get_nearby_sensors_options(
|
||||
"""Return a set of nearby sensors as SelectOptionDict objects."""
|
||||
return [
|
||||
SelectOptionDict(
|
||||
value=str(result.sensor.sensor_index),
|
||||
label=f"{result.sensor.name} ({round(result.distance, 1)} km away)",
|
||||
value=str(result.sensor.sensor_index), label=cast(str, result.sensor.name)
|
||||
)
|
||||
for result in nearby_sensor_results
|
||||
]
|
||||
@@ -118,50 +120,6 @@ def async_get_remove_sensor_schema(sensors: list[SelectOptionDict]) -> vol.Schem
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_sensor_index(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> int:
|
||||
"""Get the sensor index related to a config and device entry.
|
||||
|
||||
Note that this method expects that there will always be a single sensor index per
|
||||
DeviceEntry.
|
||||
"""
|
||||
[sensor_index] = [
|
||||
sensor_index
|
||||
for sensor_index in config_entry.options[CONF_SENSOR_INDICES]
|
||||
if (DOMAIN, str(sensor_index)) in device_entry.identifiers
|
||||
]
|
||||
|
||||
return cast(int, sensor_index)
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_sensor_by_device_id(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
device_id: str,
|
||||
*,
|
||||
remove_device: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Remove a sensor and return update config entry options."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
assert device_entry
|
||||
|
||||
removed_sensor_index = async_get_sensor_index(hass, config_entry, device_entry)
|
||||
options = deepcopy({**config_entry.options})
|
||||
options[CONF_LAST_UPDATE_SENSOR_ADD] = False
|
||||
options[CONF_SENSOR_INDICES].remove(removed_sensor_index)
|
||||
|
||||
if remove_device:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Define a validation result."""
|
||||
@@ -408,7 +366,6 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
options = deepcopy({**self.config_entry.options})
|
||||
options[CONF_LAST_UPDATE_SENSOR_ADD] = True
|
||||
options[CONF_SENSOR_INDICES].append(sensor_index)
|
||||
return self.async_create_entry(title="", data=options)
|
||||
|
||||
@@ -433,8 +390,50 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
),
|
||||
)
|
||||
|
||||
new_entry_options = async_remove_sensor_by_device_id(
|
||||
self.hass, self.config_entry, user_input[CONF_SENSOR_DEVICE_ID]
|
||||
device_registry = dr.async_get(self.hass)
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
device_id = user_input[CONF_SENSOR_DEVICE_ID]
|
||||
device_entry = cast(dr.DeviceEntry, device_registry.async_get(device_id))
|
||||
|
||||
# Determine the entity entries that belong to this device.
|
||||
entity_entries = er.async_entries_for_device(
|
||||
entity_registry, device_id, include_disabled_entities=True
|
||||
)
|
||||
|
||||
return self.async_create_entry(title="", data=new_entry_options)
|
||||
device_entities_removed_event = asyncio.Event()
|
||||
|
||||
@callback
|
||||
def async_device_entity_state_changed(_: Event) -> None:
|
||||
"""Listen and respond when all device entities are removed."""
|
||||
if all(
|
||||
self.hass.states.get(entity_entry.entity_id) is None
|
||||
for entity_entry in entity_entries
|
||||
):
|
||||
device_entities_removed_event.set()
|
||||
|
||||
# Track state changes for this device's entities and when they're removed,
|
||||
# finish the flow:
|
||||
cancel_state_track = async_track_state_change_event(
|
||||
self.hass,
|
||||
[entity_entry.entity_id for entity_entry in entity_entries],
|
||||
async_device_entity_state_changed,
|
||||
)
|
||||
device_registry.async_update_device(
|
||||
device_id, remove_config_entry_id=self.config_entry.entry_id
|
||||
)
|
||||
await device_entities_removed_event.wait()
|
||||
|
||||
# Once we're done, we can cancel the state change tracker callback:
|
||||
cancel_state_track()
|
||||
|
||||
# Build new config entry options:
|
||||
removed_sensor_index = next(
|
||||
sensor_index
|
||||
for sensor_index in self.config_entry.options[CONF_SENSOR_INDICES]
|
||||
if (DOMAIN, str(sensor_index)) in device_entry.identifiers
|
||||
)
|
||||
options = deepcopy({**self.config_entry.options})
|
||||
options[CONF_SENSOR_INDICES].remove(removed_sensor_index)
|
||||
|
||||
return self.async_create_entry(title="", data=options)
|
||||
|
||||
@@ -5,6 +5,5 @@ DOMAIN = "purpleair"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_LAST_UPDATE_SENSOR_ADD = "last_update_sensor_add"
|
||||
CONF_READ_KEY = "read_key"
|
||||
CONF_SENSOR_INDICES = "sensor_indices"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -9,19 +9,22 @@ import logging
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
import async_timeout
|
||||
from reolink_ip.exceptions import ApiError, InvalidContentTypeError
|
||||
from reolink_aio.exceptions import ApiError, InvalidContentTypeError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DEVICE_UPDATE_INTERVAL, DOMAIN, PLATFORMS
|
||||
from .const import DOMAIN
|
||||
from .host import ReolinkHost
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.CAMERA]
|
||||
DEVICE_UPDATE_INTERVAL = 60
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReolinkData:
|
||||
@@ -31,14 +34,15 @@ class ReolinkData:
|
||||
device_coordinator: DataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Reolink from a config entry."""
|
||||
host = ReolinkHost(hass, dict(entry.data), dict(entry.options))
|
||||
host = ReolinkHost(hass, config_entry.data, config_entry.options)
|
||||
|
||||
try:
|
||||
if not await host.async_init():
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while trying to setup {host.api.host}:{host.api.port}: failed to obtain data from device."
|
||||
f"Error while trying to setup {host.api.host}:{host.api.port}: "
|
||||
"failed to obtain data from device."
|
||||
)
|
||||
except (
|
||||
ClientConnectorError,
|
||||
@@ -50,14 +54,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".'
|
||||
) from err
|
||||
|
||||
entry.async_on_unload(
|
||||
config_entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop)
|
||||
)
|
||||
|
||||
async def async_device_config_update():
|
||||
"""Perform the update of the host config-state cache, and renew the ONVIF-subscription."""
|
||||
"""Update the host state cache and renew the ONVIF-subscription."""
|
||||
async with async_timeout.timeout(host.api.timeout):
|
||||
await host.update_states() # Login session is implicitly updated here, so no need to explicitly do it in a timer
|
||||
# Login session is implicitly updated here
|
||||
await host.update_states()
|
||||
|
||||
coordinator_device_config_update = DataUpdateCoordinator(
|
||||
hass,
|
||||
@@ -69,30 +74,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator_device_config_update.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ReolinkData(
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
|
||||
host=host,
|
||||
device_coordinator=coordinator_device_config_update,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(entry_update_listener))
|
||||
config_entry.async_on_unload(
|
||||
config_entry.add_update_listener(entry_update_listener)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def entry_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Update the configuration of the host entity."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
host: ReolinkHost = hass.data[DOMAIN][entry.entry_id].host
|
||||
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
|
||||
|
||||
await host.stop()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
):
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import Any
|
||||
|
||||
from reolink_ip.exceptions import ApiError, CredentialsInvalidError
|
||||
from reolink_aio.exceptions import ApiError, CredentialsInvalidError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
@@ -18,6 +18,8 @@ from .host import ReolinkHost
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL}
|
||||
|
||||
|
||||
class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Reolink options."""
|
||||
@@ -26,10 +28,12 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Initialize ReolinkOptionsFlowHandler."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None) -> FlowResult:
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the Reolink options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
@@ -37,9 +41,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
{
|
||||
vol.Required(
|
||||
CONF_PROTOCOL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_PROTOCOL, DEFAULT_PROTOCOL
|
||||
),
|
||||
default=self.config_entry.options[CONF_PROTOCOL],
|
||||
): vol.In(["rtsp", "rtmp"]),
|
||||
}
|
||||
),
|
||||
@@ -51,8 +53,6 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
host: ReolinkHost | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@@ -61,14 +61,16 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Options callback for Reolink."""
|
||||
return ReolinkOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None) -> FlowResult:
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
placeholders = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self.async_obtain_host_settings(self.hass, user_input)
|
||||
host = await async_obtain_host_settings(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors[CONF_HOST] = "cannot_connect"
|
||||
except CredentialsInvalidError:
|
||||
@@ -81,19 +83,17 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
placeholders["error"] = str(err)
|
||||
errors[CONF_HOST] = "unknown"
|
||||
|
||||
self.host = cast(ReolinkHost, self.host)
|
||||
|
||||
if not errors:
|
||||
user_input[CONF_PORT] = self.host.api.port
|
||||
user_input[CONF_USE_HTTPS] = self.host.api.use_https
|
||||
user_input[CONF_PORT] = host.api.port
|
||||
user_input[CONF_USE_HTTPS] = host.api.use_https
|
||||
|
||||
await self.async_set_unique_id(
|
||||
self.host.unique_id, raise_on_progress=False
|
||||
)
|
||||
await self.async_set_unique_id(host.unique_id, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured(updates=user_input)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=str(self.host.api.nvr_name), data=user_input
|
||||
title=str(host.api.nvr_name),
|
||||
data=user_input,
|
||||
options=DEFAULT_OPTIONS,
|
||||
)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
@@ -118,19 +118,20 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders=placeholders,
|
||||
)
|
||||
|
||||
async def async_obtain_host_settings(
|
||||
self, hass: core.HomeAssistant, user_input: dict
|
||||
):
|
||||
"""Initialize the Reolink host and get the host information."""
|
||||
host = ReolinkHost(hass, user_input, {})
|
||||
|
||||
try:
|
||||
if not await host.async_init():
|
||||
raise CannotConnect
|
||||
finally:
|
||||
await host.stop()
|
||||
async def async_obtain_host_settings(
|
||||
hass: core.HomeAssistant, user_input: dict
|
||||
) -> ReolinkHost:
|
||||
"""Initialize the Reolink host and get the host information."""
|
||||
host = ReolinkHost(hass, user_input, DEFAULT_OPTIONS)
|
||||
|
||||
self.host = host
|
||||
try:
|
||||
if not await host.async_init():
|
||||
raise CannotConnect
|
||||
finally:
|
||||
await host.stop()
|
||||
|
||||
return host
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from reolink_ip.api import Host
|
||||
from reolink_ip.exceptions import (
|
||||
from reolink_aio.api import Host
|
||||
from reolink_aio.exceptions import (
|
||||
ApiError,
|
||||
CredentialsInvalidError,
|
||||
InvalidContentTypeError,
|
||||
@@ -16,7 +18,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DEFAULT_TIMEOUT
|
||||
from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,18 +29,14 @@ class ReolinkHost:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
options: dict,
|
||||
config: Mapping[str, Any],
|
||||
options: Mapping[str, Any],
|
||||
) -> None:
|
||||
"""Initialize Reolink Host. Could be either NVR, or Camera."""
|
||||
self._hass: HomeAssistant = hass
|
||||
|
||||
self._clientsession: aiohttp.ClientSession | None = None
|
||||
self._unique_id: str | None = None
|
||||
|
||||
cur_protocol = (
|
||||
DEFAULT_PROTOCOL if CONF_PROTOCOL not in options else options[CONF_PROTOCOL]
|
||||
)
|
||||
self._unique_id: str = ""
|
||||
|
||||
self._api = Host(
|
||||
config[CONF_HOST],
|
||||
@@ -46,12 +44,12 @@ class ReolinkHost:
|
||||
config[CONF_PASSWORD],
|
||||
port=config.get(CONF_PORT),
|
||||
use_https=config.get(CONF_USE_HTTPS),
|
||||
protocol=cur_protocol,
|
||||
protocol=options[CONF_PROTOCOL],
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Create the unique ID, base for all entities."""
|
||||
return self._unique_id
|
||||
|
||||
@@ -99,23 +97,22 @@ class ReolinkHost:
|
||||
):
|
||||
if enable_onvif:
|
||||
_LOGGER.error(
|
||||
"Unable to switch on ONVIF on %s. You need it to be ON to receive notifications",
|
||||
"Failed to enable ONVIF on %s. Set it to ON to receive notifications",
|
||||
self._api.nvr_name,
|
||||
)
|
||||
|
||||
if enable_rtmp:
|
||||
_LOGGER.error(
|
||||
"Unable to switch on RTMP on %s. You need it to be ON",
|
||||
"Failed to enable RTMP on %s. Set it to ON",
|
||||
self._api.nvr_name,
|
||||
)
|
||||
elif enable_rtsp:
|
||||
_LOGGER.error(
|
||||
"Unable to switch on RTSP on %s. You need it to be ON",
|
||||
"Failed to enable RTSP on %s. Set it to ON",
|
||||
self._api.nvr_name,
|
||||
)
|
||||
|
||||
if self._unique_id is None:
|
||||
self._unique_id = format_mac(self._api.mac_address)
|
||||
self._unique_id = format_mac(self._api.mac_address)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
"name": "Reolink IP NVR/camera",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"requirements": ["reolink-ip==0.0.40"],
|
||||
"dependencies": ["webhook"],
|
||||
"after_dependencies": ["http"],
|
||||
"codeowners": ["@starkillerOG", "@JimStar"],
|
||||
"requirements": ["reolink-aio==0.1.3"],
|
||||
"codeowners": ["@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["reolink-ip"]
|
||||
"loggers": ["reolink-aio"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -86,7 +86,11 @@ from homeassistant.helpers.typing import ConfigType, StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import (
|
||||
BaseUnitConverter,
|
||||
DataRateConverter,
|
||||
DistanceConverter,
|
||||
ElectricCurrentConverter,
|
||||
ElectricPotentialConverter,
|
||||
InformationConverter,
|
||||
MassConverter,
|
||||
PressureConverter,
|
||||
SpeedConverter,
|
||||
@@ -183,7 +187,7 @@ class SensorDeviceClass(StrEnum):
|
||||
CURRENT = "current"
|
||||
"""Current.
|
||||
|
||||
Unit of measurement: `A`
|
||||
Unit of measurement: `A`, `mA`
|
||||
"""
|
||||
|
||||
DATA_RATE = "data_rate"
|
||||
@@ -305,7 +309,7 @@ class SensorDeviceClass(StrEnum):
|
||||
POWER_FACTOR = "power_factor"
|
||||
"""Power factor.
|
||||
|
||||
Unit of measurement: `%`
|
||||
Unit of measurement: `%`, `None`
|
||||
"""
|
||||
|
||||
POWER = "power"
|
||||
@@ -388,7 +392,7 @@ class SensorDeviceClass(StrEnum):
|
||||
VOLTAGE = "voltage"
|
||||
"""Voltage.
|
||||
|
||||
Unit of measurement: `V`
|
||||
Unit of measurement: `V`, `mV`
|
||||
"""
|
||||
|
||||
VOLUME = "volume"
|
||||
@@ -466,12 +470,16 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
|
||||
# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in
|
||||
# `entity-registry-settings.ts`
|
||||
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
|
||||
SensorDeviceClass.DATA_RATE: DataRateConverter,
|
||||
SensorDeviceClass.DATA_SIZE: InformationConverter,
|
||||
SensorDeviceClass.DISTANCE: DistanceConverter,
|
||||
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
|
||||
SensorDeviceClass.GAS: VolumeConverter,
|
||||
SensorDeviceClass.PRECIPITATION: DistanceConverter,
|
||||
SensorDeviceClass.PRESSURE: PressureConverter,
|
||||
SensorDeviceClass.SPEED: SpeedConverter,
|
||||
SensorDeviceClass.TEMPERATURE: TemperatureConverter,
|
||||
SensorDeviceClass.VOLTAGE: ElectricPotentialConverter,
|
||||
SensorDeviceClass.VOLUME: VolumeConverter,
|
||||
SensorDeviceClass.WATER: VolumeConverter,
|
||||
SensorDeviceClass.WEIGHT: MassConverter,
|
||||
@@ -485,7 +493,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
SensorDeviceClass.BATTERY: {PERCENTAGE},
|
||||
SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION},
|
||||
SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
|
||||
SensorDeviceClass.CURRENT: {UnitOfElectricCurrent.AMPERE},
|
||||
SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent),
|
||||
SensorDeviceClass.DATA_RATE: set(UnitOfDataRate),
|
||||
SensorDeviceClass.DATA_SIZE: set(UnitOfInformation),
|
||||
SensorDeviceClass.DISTANCE: set(UnitOfLength),
|
||||
@@ -513,7 +521,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE},
|
||||
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
|
||||
SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT},
|
||||
SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
|
||||
SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
|
||||
@@ -533,7 +541,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
SensorDeviceClass.VOLTAGE: {UnitOfElectricPotential.VOLT},
|
||||
SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential),
|
||||
SensorDeviceClass.VOLUME: set(UnitOfVolume),
|
||||
SensorDeviceClass.WATER: {
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
@@ -960,6 +968,7 @@ class SensorEntity(Entity):
|
||||
# Validate unit of measurement used for sensors with a device class
|
||||
if (
|
||||
not self._invalid_unit_of_measurement_reported
|
||||
and value is not None
|
||||
and device_class
|
||||
and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None
|
||||
and native_unit_of_measurement not in units
|
||||
|
||||
@@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [
|
||||
selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"),
|
||||
]
|
||||
|
||||
INTERNAL_WIFI_AP_IP = "192.168.33.1"
|
||||
|
||||
|
||||
async def validate_input(
|
||||
hass: HomeAssistant,
|
||||
@@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
current_entry := await self.async_set_unique_id(mac)
|
||||
) and current_entry.data[CONF_HOST] == host:
|
||||
await async_reconnect_soon(self.hass, current_entry)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
if host == INTERNAL_WIFI_AP_IP:
|
||||
# If the device is broadcasting the internal wifi ap ip
|
||||
# we can't connect to it, so we should not update the
|
||||
# entry with the new host as it will be unreachable
|
||||
#
|
||||
# This is a workaround for a bug in the firmware 0.12 (and older?)
|
||||
# which should be removed once the firmware is fixed
|
||||
# and the old version is no longer in use
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
|
||||
@@ -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,62 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_lock_auth(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the SwitchBot API auth step."""
|
||||
errors = {}
|
||||
assert self._discovered_adv is not None
|
||||
description_placeholders = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
key_details = await self.hass.async_add_executor_job(
|
||||
SwitchbotLock.retrieve_encryption_key,
|
||||
self._discovered_adv.address,
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except SwitchbotAccountConnectionError as ex:
|
||||
raise AbortFlow("cannot_connect") from ex
|
||||
except SwitchbotAuthenticationError as ex:
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
description_placeholders = {"error_detail": str(ex)}
|
||||
else:
|
||||
return await self.async_step_lock_key(key_details)
|
||||
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="lock_auth",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"name": name_from_discovery(self._discovered_adv),
|
||||
**description_placeholders,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_lock_choose_method(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the SwitchBot API chose method step."""
|
||||
assert self._discovered_adv is not None
|
||||
|
||||
return self.async_show_menu(
|
||||
step_id="lock_choose_method",
|
||||
menu_options=["lock_auth", "lock_key"],
|
||||
description_placeholders={
|
||||
"name": name_from_discovery(self._discovered_adv),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_lock_key(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
@@ -160,12 +225,11 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
if not await SwitchbotLock.verify_encryption_key(
|
||||
self._discovered_adv.device,
|
||||
user_input.get(CONF_KEY_ID),
|
||||
user_input.get(CONF_ENCRYPTION_KEY),
|
||||
user_input[CONF_KEY_ID],
|
||||
user_input[CONF_ENCRYPTION_KEY],
|
||||
):
|
||||
errors = {
|
||||
CONF_KEY_ID: "key_id_invalid",
|
||||
CONF_ENCRYPTION_KEY: "encryption_key_invalid",
|
||||
"base": "encryption_key_invalid",
|
||||
}
|
||||
else:
|
||||
return await self._async_create_entry_from_discovery(user_input)
|
||||
@@ -229,7 +293,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
device_adv = self._discovered_advs[user_input[CONF_ADDRESS]]
|
||||
await self._async_set_device(device_adv)
|
||||
if device_adv.data.get("modelName") == SwitchbotModel.LOCK:
|
||||
return await self.async_step_lock_key()
|
||||
return await self.async_step_lock_choose_method()
|
||||
if device_adv.data["isEncrypted"]:
|
||||
return await self.async_step_password()
|
||||
return await self._async_create_entry_from_discovery(user_input)
|
||||
@@ -241,7 +305,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
device_adv = list(self._discovered_advs.values())[0]
|
||||
await self._async_set_device(device_adv)
|
||||
if device_adv.data.get("modelName") == SwitchbotModel.LOCK:
|
||||
return await self.async_step_lock_key()
|
||||
return await self.async_step_lock_choose_method()
|
||||
if device_adv.data["isEncrypted"]:
|
||||
return await self.async_step_password()
|
||||
return await self.async_step_confirm()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.33.0"],
|
||||
"requirements": ["PySwitchbot==0.36.3"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": [
|
||||
|
||||
@@ -22,11 +22,25 @@
|
||||
"key_id": "Key ID",
|
||||
"encryption_key": "Encryption key"
|
||||
}
|
||||
},
|
||||
"lock_auth": {
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"lock_choose_method": {
|
||||
"description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
|
||||
"menu_options": {
|
||||
"lock_auth": "SwitchBot account (recommended)",
|
||||
"lock_key": "Enter lock encryption key manually"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"key_id_invalid": "Key ID or Encryption key is invalid",
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid"
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid",
|
||||
"auth_failed": "Authentication failed: {error_detail}"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
|
||||
@@ -7,11 +7,36 @@
|
||||
"switchbot_unsupported_type": "Unsupported Switchbot Type.",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"error": {
|
||||
"auth_failed": "Authentication failed: {error_detail}",
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid"
|
||||
},
|
||||
"flow_title": "{name} ({address})",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to set up {name}?"
|
||||
},
|
||||
"lock_auth": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive."
|
||||
},
|
||||
"lock_choose_method": {
|
||||
"description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
|
||||
"menu_options": {
|
||||
"lock_auth": "SwitchBot account (recommended)",
|
||||
"lock_key": "Enter lock encryption key manually"
|
||||
}
|
||||
},
|
||||
"lock_key": {
|
||||
"data": {
|
||||
"encryption_key": "Encryption key",
|
||||
"key_id": "Key ID"
|
||||
},
|
||||
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation."
|
||||
},
|
||||
"password": {
|
||||
"data": {
|
||||
"password": "Password"
|
||||
@@ -22,18 +47,7 @@
|
||||
"data": {
|
||||
"address": "Device address"
|
||||
}
|
||||
},
|
||||
"lock_key": {
|
||||
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.",
|
||||
"data": {
|
||||
"key_id": "Key ID",
|
||||
"encryption_key": "Encryption key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"key_id_invalid": "Key ID or Encryption key is invalid",
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -45,4 +59,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Tasmota",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tasmota",
|
||||
"requirements": ["hatasmota==0.6.1"],
|
||||
"requirements": ["hatasmota==0.6.2"],
|
||||
"dependencies": ["mqtt"],
|
||||
"mqtt": ["tasmota/discovery/#"],
|
||||
"codeowners": ["@emontnemery"],
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
POWER_VOLT_AMPERE_REACTIVE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfApparentPower,
|
||||
@@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = {
|
||||
hc.LIGHT_LUX: LIGHT_LUX,
|
||||
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
|
||||
hc.PERCENTAGE: PERCENTAGE,
|
||||
hc.POWER_FACTOR: None,
|
||||
hc.POWER_WATT: UnitOfPower.WATT,
|
||||
hc.PRESSURE_HPA: UnitOfPressure.HPA,
|
||||
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
|
||||
hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
|
||||
hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"domain": "tibber",
|
||||
"name": "Tibber",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"requirements": ["pyTibber==0.26.6"],
|
||||
"requirements": ["pyTibber==0.26.7"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"quality_scale": "silver",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -65,6 +65,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
|
||||
self.entity_description = description
|
||||
|
||||
self._removed = False
|
||||
self._write_state = False
|
||||
|
||||
self._attr_available = description.available_fn(controller, obj_id)
|
||||
self._attr_device_info = description.device_info_fn(controller.api, obj_id)
|
||||
@@ -117,9 +118,14 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
self._attr_available = description.available_fn(self.controller, self._obj_id)
|
||||
if (
|
||||
available := description.available_fn(self.controller, self._obj_id)
|
||||
) != self.available:
|
||||
self._attr_available = available
|
||||
self._write_state = True
|
||||
self.async_update_state(event, obj_id)
|
||||
self.async_write_ha_state()
|
||||
if self._write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_signal_reachable_callback(self) -> None:
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
|
||||
self.async_on_remove(
|
||||
handler.subscribe(
|
||||
self.async_signalling_callback,
|
||||
id_filter=self._obj_id,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
@@ -253,11 +254,19 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
update_state = False
|
||||
|
||||
obj = description.object_fn(self.controller.api, self._obj_id)
|
||||
if (value := description.value_fn(self.controller, obj)) != self.native_value:
|
||||
self._attr_native_value = value
|
||||
self._attr_available = description.available_fn(self.controller, self._obj_id)
|
||||
self.async_write_ha_state()
|
||||
update_state = True
|
||||
if (
|
||||
available := description.available_fn(self.controller, self._obj_id)
|
||||
) != self.available:
|
||||
self._attr_available = available
|
||||
update_state = True
|
||||
if update_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_signal_reachable_callback(self) -> None:
|
||||
|
||||
@@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
|
||||
self.async_on_remove(
|
||||
handler.subscribe(
|
||||
self.async_signalling_callback,
|
||||
id_filter=self._obj_id,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
@@ -410,11 +411,20 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
update_state = False
|
||||
|
||||
if not description.only_event_for_state_change:
|
||||
obj = description.object_fn(self.controller.api, self._obj_id)
|
||||
self._attr_is_on = description.is_on_fn(self.controller.api, obj)
|
||||
self._attr_available = description.available_fn(self.controller, self._obj_id)
|
||||
self.async_write_ha_state()
|
||||
if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on:
|
||||
self._attr_is_on = is_on
|
||||
update_state = True
|
||||
if (
|
||||
available := description.available_fn(self.controller, self._obj_id)
|
||||
) != self.available:
|
||||
self._attr_available = available
|
||||
update_state = True
|
||||
if update_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_signal_reachable_callback(self) -> None:
|
||||
|
||||
@@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity):
|
||||
description = self.entity_description
|
||||
|
||||
obj = description.object_fn(self.controller.api, self._obj_id)
|
||||
self._attr_in_progress = description.state_fn(self.controller.api, obj)
|
||||
if (
|
||||
in_progress := description.state_fn(self.controller.api, obj)
|
||||
) != self.in_progress:
|
||||
self._attr_in_progress = in_progress
|
||||
self._write_state = True
|
||||
self._attr_installed_version = obj.version
|
||||
self._attr_latest_version = obj.upgrade_to_firmware or obj.version
|
||||
if self.installed_version != self.latest_version:
|
||||
self._write_state = True
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "xmpp",
|
||||
"name": "Jabber (XMPP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/xmpp",
|
||||
"requirements": ["slixmpp==1.8.2"],
|
||||
"requirements": ["slixmpp==1.8.3"],
|
||||
"codeowners": ["@fabaff", "@flowolf"],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pyasn1", "slixmpp"]
|
||||
|
||||
@@ -17,6 +17,7 @@ from zigpy.application import ControllerApplication
|
||||
from zigpy.config import CONF_DEVICE
|
||||
import zigpy.device
|
||||
import zigpy.endpoint
|
||||
import zigpy.exceptions
|
||||
import zigpy.group
|
||||
from zigpy.types.named import EUI64
|
||||
|
||||
@@ -24,6 +25,7 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH
|
||||
from homeassistant.components.system_log import LogEntry, _figure_out_source
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -172,6 +174,8 @@ class ZHAGateway:
|
||||
self.application_controller = await app_controller_cls.new(
|
||||
app_config, auto_form=True, start_radio=True
|
||||
)
|
||||
except zigpy.exceptions.TransientConnectionError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
_LOGGER.warning(
|
||||
"Couldn't start %s coordinator (attempt %s of %s)",
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zha",
|
||||
"requirements": [
|
||||
"bellows==0.34.5",
|
||||
"bellows==0.34.6",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.89",
|
||||
"zha-quirks==0.0.90",
|
||||
"zigpy-deconz==0.19.2",
|
||||
"zigpy==0.52.3",
|
||||
"zigpy==0.53.0",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.2"
|
||||
|
||||
@@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"):
|
||||
"""RSSI sensor for a device."""
|
||||
|
||||
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_should_poll = True # BaseZhaEntity defaults to False
|
||||
|
||||
@@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
PRESSURE_SENSORS,
|
||||
SIGNAL_STRENGTH_SENSORS,
|
||||
TEMPERATURE_SENSORS,
|
||||
UNIT_A_WEIGHTED_DECIBELS,
|
||||
UNIT_AMPERE as SENSOR_UNIT_AMPERE,
|
||||
UNIT_BTU_H,
|
||||
UNIT_CELSIUS,
|
||||
@@ -52,6 +53,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
UNIT_INCHES_PER_HOUR,
|
||||
UNIT_KILOGRAM,
|
||||
UNIT_KILOHERTZ,
|
||||
UNIT_KILOPASCAL,
|
||||
UNIT_LITER,
|
||||
UNIT_LUX,
|
||||
UNIT_M_S,
|
||||
@@ -69,6 +71,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
UNIT_RSSI,
|
||||
UNIT_SECOND,
|
||||
UNIT_SYSTOLIC,
|
||||
UNIT_UV_INDEX,
|
||||
UNIT_VOLT as SENSOR_UNIT_VOLT,
|
||||
UNIT_WATT as SENSOR_UNIT_WATT,
|
||||
UNIT_WATT_PER_SQUARE_METER,
|
||||
@@ -94,8 +97,8 @@ from homeassistant.const import (
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UV_INDEX,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -105,6 +108,7 @@ from homeassistant.const import (
|
||||
UnitOfMass,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
@@ -134,7 +138,7 @@ from .const import (
|
||||
)
|
||||
from .helpers import ZwaveValueID
|
||||
|
||||
METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
|
||||
METER_DEVICE_CLASS_MAP: dict[str, list[MeterScaleType]] = {
|
||||
ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES,
|
||||
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_METER_TYPES,
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: ENERGY_TOTAL_INCREASING_METER_TYPES,
|
||||
@@ -142,7 +146,7 @@ METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
|
||||
ENTITY_DESC_KEY_POWER_FACTOR: POWER_FACTOR_METER_TYPES,
|
||||
}
|
||||
|
||||
MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = {
|
||||
MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = {
|
||||
ENTITY_DESC_KEY_CO: CO_SENSORS,
|
||||
ENTITY_DESC_KEY_CO2: CO2_SENSORS,
|
||||
ENTITY_DESC_KEY_CURRENT: CURRENT_SENSORS,
|
||||
@@ -156,7 +160,7 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = {
|
||||
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS,
|
||||
}
|
||||
|
||||
METER_UNIT_MAP: dict[str, set[MeterScaleType]] = {
|
||||
METER_UNIT_MAP: dict[str, list[MeterScaleType]] = {
|
||||
UnitOfElectricCurrent.AMPERE: METER_UNIT_AMPERE,
|
||||
UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER,
|
||||
@@ -166,7 +170,7 @@ METER_UNIT_MAP: dict[str, set[MeterScaleType]] = {
|
||||
UnitOfPower.WATT: METER_UNIT_WATT,
|
||||
}
|
||||
|
||||
MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
MULTILEVEL_SENSOR_UNIT_MAP: dict[str, list[MultilevelSensorScaleType]] = {
|
||||
UnitOfElectricCurrent.AMPERE: SENSOR_UNIT_AMPERE,
|
||||
UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H,
|
||||
UnitOfTemperature.CELSIUS: UNIT_CELSIUS,
|
||||
@@ -174,17 +178,19 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE,
|
||||
UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER,
|
||||
UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR,
|
||||
SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL,
|
||||
UnitOfSoundPressure.DECIBEL: UNIT_DECIBEL,
|
||||
UnitOfSoundPressure.WEIGHTED_DECIBEL_A: UNIT_A_WEIGHTED_DECIBELS,
|
||||
DEGREE: UNIT_DEGREES,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: {
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: [
|
||||
*UNIT_DENSITY,
|
||||
*UNIT_MICROGRAM_PER_CUBIC_METER,
|
||||
},
|
||||
],
|
||||
UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT,
|
||||
UnitOfLength.FEET: UNIT_FEET,
|
||||
UnitOfVolume.GALLONS: UNIT_GALLONS,
|
||||
UnitOfFrequency.HERTZ: UNIT_HERTZ,
|
||||
UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY,
|
||||
UnitOfPressure.KPA: UNIT_KILOPASCAL,
|
||||
UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
|
||||
UnitOfMass.KILOGRAMS: UNIT_KILOGRAM,
|
||||
UnitOfFrequency.KILOHERTZ: UNIT_KILOHERTZ,
|
||||
@@ -197,7 +203,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH,
|
||||
UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S,
|
||||
CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION,
|
||||
PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI},
|
||||
PERCENTAGE: [*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI],
|
||||
UnitOfMass.POUNDS: UNIT_POUNDS,
|
||||
UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL,
|
||||
@@ -206,6 +212,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
UnitOfElectricPotential.VOLT: SENSOR_UNIT_VOLT,
|
||||
UnitOfPower.WATT: SENSOR_UNIT_WATT,
|
||||
UnitOfIrradiance.WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER,
|
||||
UV_INDEX: UNIT_UV_INDEX,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -319,9 +326,9 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate):
|
||||
enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType,
|
||||
set_map: Mapping[
|
||||
str,
|
||||
set[MultilevelSensorType]
|
||||
| set[MultilevelSensorScaleType]
|
||||
| set[MeterScaleType],
|
||||
list[MultilevelSensorType]
|
||||
| list[MultilevelSensorScaleType]
|
||||
| list[MeterScaleType],
|
||||
],
|
||||
) -> str | None:
|
||||
"""Find a key in a set map that matches a given enum value."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.1"],
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.44.0"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["usb", "http", "websocket_api"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -24,6 +24,18 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_platform
|
||||
@@ -76,98 +88,207 @@ STATUS_ICON: dict[NodeStatus, str] = {
|
||||
}
|
||||
|
||||
|
||||
ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = {
|
||||
ENTITY_DESC_KEY_BATTERY: SensorEntityDescription(
|
||||
# These descriptions should include device class.
|
||||
ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
|
||||
tuple[str, str], SensorEntityDescription
|
||||
] = {
|
||||
(ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_BATTERY,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
ENTITY_DESC_KEY_CURRENT: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CURRENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
),
|
||||
ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
UnitOfElectricPotential.MILLIVOLT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription(
|
||||
(
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
ENTITY_DESC_KEY_POWER: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
),
|
||||
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_POWER_FACTOR,
|
||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
ENTITY_DESC_KEY_CO: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CO,
|
||||
device_class=SensorDeviceClass.CO,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
ENTITY_DESC_KEY_CO2: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CO2,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_HUMIDITY,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ILLUMINANCE,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
),
|
||||
ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
),
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
),
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
),
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
),
|
||||
ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription(
|
||||
(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=None,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
),
|
||||
}
|
||||
|
||||
# These descriptions are without device class.
|
||||
ENTITY_DESCRIPTION_KEY_MAP = {
|
||||
ENTITY_DESC_KEY_CO: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CO,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_MEASUREMENT,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_entity_description(
|
||||
data: NumericSensorDataTemplateData,
|
||||
) -> SensorEntityDescription:
|
||||
"""Return the entity description for the given data."""
|
||||
data_description_key = data.entity_description_key or ""
|
||||
data_unit = data.unit_of_measurement or ""
|
||||
return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get(
|
||||
(data_description_key, data_unit),
|
||||
ENTITY_DESCRIPTION_KEY_MAP.get(
|
||||
data_description_key,
|
||||
SensorEntityDescription(
|
||||
"base_sensor", native_unit_of_measurement=data.unit_of_measurement
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -187,9 +308,8 @@ async def async_setup_entry(
|
||||
data: NumericSensorDataTemplateData = info.platform_data
|
||||
else:
|
||||
data = NumericSensorDataTemplateData()
|
||||
entity_description = ENTITY_DESCRIPTION_KEY_MAP.get(
|
||||
data.entity_description_key or "", SensorEntityDescription("base_sensor")
|
||||
)
|
||||
|
||||
entity_description = get_entity_description(data)
|
||||
|
||||
if info.platform_hint == "string_sensor":
|
||||
entities.append(
|
||||
@@ -308,11 +428,9 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
@callback
|
||||
def on_value_update(self) -> None:
|
||||
"""Handle scale changes for this value on value updated event."""
|
||||
self._attr_native_unit_of_measurement = (
|
||||
NumericSensorDataTemplate()
|
||||
.resolve_data(self.info.primary_value)
|
||||
.unit_of_measurement
|
||||
)
|
||||
data = NumericSensorDataTemplate().resolve_data(self.info.primary_value)
|
||||
self.entity_description = get_entity_description(data)
|
||||
self._attr_native_unit_of_measurement = data.unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -324,6 +442,8 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return unit of measurement the value is expressed in."""
|
||||
if self.entity_description.native_unit_of_measurement is not None:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
if self._attr_native_unit_of_measurement is not None:
|
||||
return self._attr_native_unit_of_measurement
|
||||
if self.info.primary_value.metadata.unit is None:
|
||||
|
||||
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 1
|
||||
PATCH_VERSION: Final = "0b2"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
||||
@@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry"
|
||||
EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated"
|
||||
STORAGE_KEY = "core.device_registry"
|
||||
STORAGE_VERSION_MAJOR = 1
|
||||
STORAGE_VERSION_MINOR = 4
|
||||
STORAGE_VERSION_MINOR = 3
|
||||
SAVE_DELAY = 10
|
||||
CLEANUP_DELAY = 10
|
||||
|
||||
@@ -70,7 +70,6 @@ class DeviceEntryType(StrEnum):
|
||||
class DeviceEntry:
|
||||
"""Device Registry Entry."""
|
||||
|
||||
aliases: set[str] = attr.ib(factory=set)
|
||||
area_id: str | None = attr.ib(default=None)
|
||||
config_entries: set[str] = attr.ib(converter=set, factory=set)
|
||||
configuration_url: str | None = attr.ib(default=None)
|
||||
@@ -175,9 +174,6 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
|
||||
# Version 1.3 adds hw_version
|
||||
for device in old_data["devices"]:
|
||||
device["hw_version"] = None
|
||||
if old_minor_version < 4:
|
||||
for device in old_data["devices"]:
|
||||
device["aliases"] = []
|
||||
|
||||
if old_major_version > 1:
|
||||
raise NotImplementedError
|
||||
@@ -382,7 +378,6 @@ class DeviceRegistry:
|
||||
device_id: str,
|
||||
*,
|
||||
add_config_entry_id: str | UndefinedType = UNDEFINED,
|
||||
aliases: set[str] | UndefinedType = UNDEFINED,
|
||||
area_id: str | None | UndefinedType = UNDEFINED,
|
||||
configuration_url: str | None | UndefinedType = UNDEFINED,
|
||||
disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED,
|
||||
@@ -473,7 +468,6 @@ class DeviceRegistry:
|
||||
old_values["identifiers"] = old.identifiers
|
||||
|
||||
for attr_name, value in (
|
||||
("aliases", aliases),
|
||||
("area_id", area_id),
|
||||
("configuration_url", configuration_url),
|
||||
("disabled_by", disabled_by),
|
||||
@@ -552,7 +546,6 @@ class DeviceRegistry:
|
||||
if data is not None:
|
||||
for device in data["devices"]:
|
||||
devices[device["id"]] = DeviceEntry(
|
||||
aliases=set(device["aliases"]),
|
||||
area_id=device["area_id"],
|
||||
config_entries=set(device["config_entries"]),
|
||||
configuration_url=device["configuration_url"],
|
||||
@@ -600,7 +593,6 @@ class DeviceRegistry:
|
||||
|
||||
data["devices"] = [
|
||||
{
|
||||
"aliases": list(entry.aliases),
|
||||
"area_id": entry.area_id,
|
||||
"config_entries": list(entry.config_entries),
|
||||
"configuration_url": entry.configuration_url,
|
||||
|
||||
@@ -21,9 +21,9 @@ cryptography==38.0.3
|
||||
dbus-fast==1.82.0
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.61.0
|
||||
home-assistant-bluetooth==1.9.0
|
||||
home-assistant-frontend==20221228.0
|
||||
httpx==0.23.1
|
||||
home-assistant-bluetooth==1.9.2
|
||||
home-assistant-frontend==20230104.0
|
||||
httpx==0.23.2
|
||||
ifaddr==0.1.7
|
||||
janus==1.0.0
|
||||
jinja2==3.1.2
|
||||
@@ -90,7 +90,7 @@ regex==2021.8.28
|
||||
# requirements so we can directly link HA versions to these library versions.
|
||||
anyio==3.6.2
|
||||
h11==0.14.0
|
||||
httpcore==0.16.2
|
||||
httpcore==0.16.3
|
||||
|
||||
# Ensure we have a hyperframe version that works in Python 3.10
|
||||
# 5.2.0 fixed a collections abc deprecation
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.1.0b2"
|
||||
version = "2023.1.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@@ -35,8 +35,8 @@ dependencies = [
|
||||
"ciso8601==2.3.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.23.1",
|
||||
"home-assistant-bluetooth==1.9.0",
|
||||
"httpx==0.23.2",
|
||||
"home-assistant-bluetooth==1.9.2",
|
||||
"ifaddr==0.1.7",
|
||||
"jinja2==3.1.2",
|
||||
"lru-dict==1.1.8",
|
||||
|
||||
@@ -10,8 +10,8 @@ awesomeversion==22.9.0
|
||||
bcrypt==3.1.7
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.0
|
||||
httpx==0.23.1
|
||||
home-assistant-bluetooth==1.9.0
|
||||
httpx==0.23.2
|
||||
home-assistant-bluetooth==1.9.2
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
lru-dict==1.1.8
|
||||
|
||||
@@ -40,7 +40,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.33.0
|
||||
PySwitchbot==0.36.3
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -86,7 +86,7 @@ adb-shell[async]==0.4.3
|
||||
adext==0.4.2
|
||||
|
||||
# homeassistant.components.adguard
|
||||
adguardhome==0.5.1
|
||||
adguardhome==0.6.1
|
||||
|
||||
# homeassistant.components.advantage_air
|
||||
advantage_air==0.4.1
|
||||
@@ -288,7 +288,7 @@ aiosyncthing==0.5.1
|
||||
aiotractive==0.5.5
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==42
|
||||
aiounifi==43
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@@ -419,10 +419,10 @@ beautifulsoup4==4.11.1
|
||||
# beewi_smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.5
|
||||
bellows==0.34.6
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.10.4
|
||||
bimmer_connected==0.12.0
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@@ -488,7 +488,7 @@ brunt==1.2.0
|
||||
bt_proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.4.0
|
||||
bthome-ble==2.4.1
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -741,10 +741,10 @@ fritzconnection==1.10.3
|
||||
gTTS==2.2.4
|
||||
|
||||
# homeassistant.components.google_assistant_sdk
|
||||
gassist-text==0.0.5
|
||||
gassist-text==0.0.7
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==4.1.0
|
||||
gcal-sync==4.1.1
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@@ -858,7 +858,7 @@ hass-nabucasa==0.61.0
|
||||
hass_splunk==0.1.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.6.1
|
||||
hatasmota==0.6.2
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate==0.10.4
|
||||
@@ -888,7 +888,7 @@ hole==0.8.0
|
||||
holidays==0.17.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221228.0
|
||||
home-assistant-frontend==20230104.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -930,7 +930,7 @@ ibm-watson==5.2.2
|
||||
ibmiotf==0.3.4
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
ical==4.2.8
|
||||
ical==4.2.9
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@@ -1029,7 +1029,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.life360
|
||||
life360==5.3.0
|
||||
life360==5.5.0
|
||||
|
||||
# homeassistant.components.osramlightify
|
||||
lightify==1.0.7.3
|
||||
@@ -1119,7 +1119,7 @@ moat-ble==0.1.1
|
||||
moehlenhoff-alpha2==1.2.1
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.6.13
|
||||
motionblinds==0.6.15
|
||||
|
||||
# homeassistant.components.motioneye
|
||||
motioneye-client==0.3.12
|
||||
@@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0
|
||||
pySwitchmate==0.5.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.26.6
|
||||
pyTibber==0.26.7
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.7.0
|
||||
@@ -1473,7 +1473,7 @@ pyalmond==0.0.2
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==7.4.0
|
||||
pyatmo==7.5.0
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyatome==0.1.1
|
||||
@@ -1500,10 +1500,10 @@ pyblackbird==0.5
|
||||
pybotvac==0.0.23
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
pybravia==0.2.3
|
||||
pybravia==0.2.5
|
||||
|
||||
# homeassistant.components.nissan_leaf
|
||||
pycarwings2==2.13
|
||||
pycarwings2==2.14
|
||||
|
||||
# homeassistant.components.cloudflare
|
||||
pycfdns==2.0.1
|
||||
@@ -1542,7 +1542,7 @@ pydaikin==2.8.0
|
||||
pydanfossair==0.1.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==105
|
||||
pydeconz==106
|
||||
|
||||
# homeassistant.components.delijn
|
||||
pydelijn==1.0.0
|
||||
@@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0
|
||||
pyebox==1.1.4
|
||||
|
||||
# homeassistant.components.econet
|
||||
pyeconet==0.1.15
|
||||
pyeconet==0.1.18
|
||||
|
||||
# homeassistant.components.edimax
|
||||
pyedimax==0.2.1
|
||||
@@ -1832,7 +1832,7 @@ pyownet==0.10.0.post1
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.15
|
||||
pypck==0.7.16
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
@@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.11
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-ip==0.0.40
|
||||
reolink-aio==0.1.3
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -2220,7 +2220,7 @@ rokuecp==0.17.0
|
||||
roombapy==1.6.5
|
||||
|
||||
# homeassistant.components.roon
|
||||
roonapi==0.1.1
|
||||
roonapi==0.1.2
|
||||
|
||||
# homeassistant.components.rova
|
||||
rova==0.3.0
|
||||
@@ -2308,7 +2308,7 @@ sisyphus-control==3.1.2
|
||||
slackclient==2.5.0
|
||||
|
||||
# homeassistant.components.xmpp
|
||||
slixmpp==1.8.2
|
||||
slixmpp==1.8.3
|
||||
|
||||
# homeassistant.components.smart_meter_texas
|
||||
smart-meter-texas==0.4.7
|
||||
@@ -2647,7 +2647,7 @@ zengge==0.2
|
||||
zeroconf==0.47.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.89
|
||||
zha-quirks==0.0.90
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
@@ -2668,13 +2668,13 @@ zigpy-zigate==0.10.3
|
||||
zigpy-znp==0.9.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.52.3
|
||||
zigpy==0.53.0
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.43.1
|
||||
zwave-js-server-python==0.44.0
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.3.0
|
||||
|
||||
@@ -36,7 +36,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.33.0
|
||||
PySwitchbot==0.36.3
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -76,7 +76,7 @@ adb-shell[async]==0.4.3
|
||||
adext==0.4.2
|
||||
|
||||
# homeassistant.components.adguard
|
||||
adguardhome==0.5.1
|
||||
adguardhome==0.6.1
|
||||
|
||||
# homeassistant.components.advantage_air
|
||||
advantage_air==0.4.1
|
||||
@@ -263,7 +263,7 @@ aiosyncthing==0.5.1
|
||||
aiotractive==0.5.5
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==42
|
||||
aiounifi==43
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@@ -346,10 +346,10 @@ base36==0.1.1
|
||||
beautifulsoup4==4.11.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.5
|
||||
bellows==0.34.6
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.10.4
|
||||
bimmer_connected==0.12.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.13.0
|
||||
@@ -392,7 +392,7 @@ brother==2.1.1
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.4.0
|
||||
bthome-ble==2.4.1
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
@@ -557,10 +557,10 @@ fritzconnection==1.10.3
|
||||
gTTS==2.2.4
|
||||
|
||||
# homeassistant.components.google_assistant_sdk
|
||||
gassist-text==0.0.5
|
||||
gassist-text==0.0.7
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==4.1.0
|
||||
gcal-sync==4.1.1
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
@@ -647,7 +647,7 @@ habitipy==0.2.0
|
||||
hass-nabucasa==0.61.0
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.6.1
|
||||
hatasmota==0.6.2
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate==0.10.4
|
||||
@@ -668,7 +668,7 @@ hole==0.8.0
|
||||
holidays==0.17.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221228.0
|
||||
home-assistant-frontend==20230104.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -695,7 +695,7 @@ iaqualink==0.5.0
|
||||
ibeacon_ble==1.0.1
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
ical==4.2.8
|
||||
ical==4.2.9
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@@ -767,7 +767,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.life360
|
||||
life360==5.3.0
|
||||
life360==5.5.0
|
||||
|
||||
# homeassistant.components.logi_circle
|
||||
logi_circle==0.2.3
|
||||
@@ -821,7 +821,7 @@ moat-ble==0.1.1
|
||||
moehlenhoff-alpha2==1.2.1
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.6.13
|
||||
motionblinds==0.6.15
|
||||
|
||||
# homeassistant.components.motioneye
|
||||
motioneye-client==0.3.12
|
||||
@@ -1039,7 +1039,7 @@ pyMetno==0.9.0
|
||||
pyRFXtrx==0.30.0
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.26.6
|
||||
pyTibber==0.26.7
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py_nextbusnext==0.1.5
|
||||
@@ -1061,7 +1061,7 @@ pyalmond==0.0.2
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==7.4.0
|
||||
pyatmo==7.5.0
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.10.3
|
||||
@@ -1079,7 +1079,7 @@ pyblackbird==0.5
|
||||
pybotvac==0.0.23
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
pybravia==0.2.3
|
||||
pybravia==0.2.5
|
||||
|
||||
# homeassistant.components.cloudflare
|
||||
pycfdns==2.0.1
|
||||
@@ -1097,7 +1097,7 @@ pycoolmasternet-async==0.1.2
|
||||
pydaikin==2.8.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==105
|
||||
pydeconz==106
|
||||
|
||||
# homeassistant.components.dexcom
|
||||
pydexcom==0.2.3
|
||||
@@ -1106,7 +1106,7 @@ pydexcom==0.2.3
|
||||
pydroid-ipcam==2.0.0
|
||||
|
||||
# homeassistant.components.econet
|
||||
pyeconet==0.1.15
|
||||
pyeconet==0.1.18
|
||||
|
||||
# homeassistant.components.efergy
|
||||
pyefergy==22.1.1
|
||||
@@ -1306,7 +1306,7 @@ pyowm==3.2.0
|
||||
pyownet==0.10.0.post1
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.15
|
||||
pypck==0.7.16
|
||||
|
||||
# homeassistant.components.plaato
|
||||
pyplaato==0.0.18
|
||||
@@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.11
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-ip==0.0.40
|
||||
reolink-aio==0.1.3
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -1547,7 +1547,7 @@ rokuecp==0.17.0
|
||||
roombapy==1.6.5
|
||||
|
||||
# homeassistant.components.roon
|
||||
roonapi==0.1.1
|
||||
roonapi==0.1.2
|
||||
|
||||
# homeassistant.components.rpi_power
|
||||
rpi-bad-power==0.1.0
|
||||
@@ -1854,7 +1854,7 @@ zamg==0.2.2
|
||||
zeroconf==0.47.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.89
|
||||
zha-quirks==0.0.90
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.2
|
||||
@@ -1869,10 +1869,10 @@ zigpy-zigate==0.10.3
|
||||
zigpy-znp==0.9.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.52.3
|
||||
zigpy==0.53.0
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.43.1
|
||||
zwave-js-server-python==0.44.0
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.3.0
|
||||
|
||||
@@ -101,7 +101,7 @@ regex==2021.8.28
|
||||
# requirements so we can directly link HA versions to these library versions.
|
||||
anyio==3.6.2
|
||||
h11==0.14.0
|
||||
httpcore==0.16.2
|
||||
httpcore==0.16.3
|
||||
|
||||
# Ensure we have a hyperframe version that works in Python 3.10
|
||||
# 5.2.0 fixed a collections abc deprecation
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Define tests for the AirVisual config flow."""
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyairvisual.cloud_api import (
|
||||
InvalidKeyError,
|
||||
@@ -21,6 +21,7 @@ from homeassistant.components.airvisual import (
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
)
|
||||
from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
@@ -31,8 +32,7 @@ from homeassistant.const import (
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -169,42 +169,41 @@ async def test_migration_1_2(hass, config, config_entry, setup_airvisual, unique
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config,config_entry_version,unique_id",
|
||||
[
|
||||
(
|
||||
{
|
||||
CONF_IP_ADDRESS: "192.168.1.100",
|
||||
CONF_PASSWORD: "abcde12345",
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO,
|
||||
},
|
||||
2,
|
||||
"192.16.1.100",
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_migration_2_3(hass, config, config_entry, unique_id):
|
||||
async def test_migration_2_3(hass, pro):
|
||||
"""Test migrating from version 2 to 3."""
|
||||
old_pro_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="192.168.1.100",
|
||||
data={
|
||||
CONF_IP_ADDRESS: "192.168.1.100",
|
||||
CONF_PASSWORD: "abcde12345",
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO,
|
||||
},
|
||||
version=2,
|
||||
)
|
||||
old_pro_entry.add_to_hass(hass)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
name="192.168.1.100",
|
||||
config_entry_id=old_pro_entry.entry_id,
|
||||
identifiers={(DOMAIN, "ABCDE12345")},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airvisual.automation.automations_with_device",
|
||||
return_value=["automation.test_automation"],
|
||||
), patch(
|
||||
"homeassistant.components.airvisual.async_get_pro_config_entry_by_ip_address",
|
||||
return_value=MockConfigEntry(
|
||||
domain="airvisual_pro",
|
||||
unique_id="192.168.1.100",
|
||||
data={CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "abcde12345"},
|
||||
version=3,
|
||||
),
|
||||
"homeassistant.components.airvisual_pro.NodeSamba", return_value=pro
|
||||
), patch(
|
||||
"homeassistant.components.airvisual.async_get_pro_device_by_config_entry",
|
||||
return_value=Mock(id="abcde12345"),
|
||||
"homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.config_entries.async_setup(old_pro_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
airvisual_config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(airvisual_config_entries) == 0
|
||||
for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)):
|
||||
entries = hass.config_entries.async_entries(domain)
|
||||
assert len(entries) == entry_count
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user