forked from home-assistant/core
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf53a9743f | |||
| 4884891b2c | |||
| 30504fc9bd | |||
| 8827454dbd | |||
| 3b30bbb85e | |||
| df9eb482b5 | |||
| 32aee61441 | |||
| 35873cbe27 | |||
| 6fe492a51c | |||
| b1bc35f1c3 | |||
| 56d10a0a7a | |||
| d091936ac6 | |||
| 1dfd4e80b9 | |||
| d919de6734 | |||
| 3f9f0f8ac2 | |||
| bf20ffae96 | |||
| dad81927cb | |||
| 92392ab3d4 | |||
| a47e5398f0 | |||
| cf6d33635b | |||
| 6a4031a383 | |||
| 2b40844171 | |||
| 9b90df74a6 | |||
| dcdf033fa9 | |||
| 4c3ae395a4 | |||
| 333ada7670 | |||
| 4fd4ba7813 | |||
| 7e96666dc5 | |||
| e463d5d16f | |||
| f28579357e | |||
| d40a9bd9ef | |||
| 28ecee6479 | |||
| 512ac7d572 | |||
| 22b353f7d5 | |||
| 49c40cd902 | |||
| 629c7a53ce | |||
| 66e3ffffa7 | |||
| 139b424717 | |||
| 33633f885d | |||
| 759a2b84f5 | |||
| ebffcb455f | |||
| 08773cefb7 | |||
| 79352ea0f0 | |||
| b7038d4eb7 | |||
| 8a310cbbf8 | |||
| 07196b0fda | |||
| 0a38af7e48 | |||
| 155fafb735 | |||
| 54ec41f25d | |||
| f480cc3396 | |||
| 2aea738032 | |||
| ab5165fdfa | |||
| c6468aca2b | |||
| 895ffbabf7 | |||
| 3f1286b338 | |||
| d3a577ad89 | |||
| f44103ac7f | |||
| f1ebda7c6f | |||
| 905769f0e8 | |||
| 43899b6f28 | |||
| b5e7da4262 | |||
| 3dc0ca7e1e | |||
| 42c46a15b4 | |||
| 97a725c2c6 | |||
| c3499e5294 | |||
| 110935461e | |||
| be40db3dff | |||
| c3c500955a | |||
| 1e5a5925e6 | |||
| d956e4b11d | |||
| 8ff8cd8b65 | |||
| fab35f227d | |||
| e4d19541f5 | |||
| 6b6fc6bbeb | |||
| f2bafee84a | |||
| 4e0cdb0537 | |||
| 79c919f62d | |||
| b6dec11487 | |||
| e2073d7762 | |||
| d7428786cd | |||
| 673bdcc556 | |||
| e8ef990e72 | |||
| 0d155c416a | |||
| e48be5c406 | |||
| 787a1613ec | |||
| bb847b346d | |||
| e9b34eaad0 | |||
| 572347025b | |||
| 29e80e56c6 | |||
| b60b2fdd7c | |||
| aaf3f61675 | |||
| 5bf972ff16 | |||
| 8eb52edabf | |||
| 4326689f52 | |||
| 06838c0280 | |||
| f97d96e3ae | |||
| ee960933db | |||
| 2ea0c54788 | |||
| dd18672341 | |||
| ac4ae0430e | |||
| eeb63d42a0 | |||
| 9d48f36754 | |||
| 157198bf41 | |||
| be25b9d4d0 | |||
| e08b71086f | |||
| 9677c6e24c | |||
| e2cda54473 | |||
| 3ca49dc8a6 | |||
| 80bc70771e | |||
| 7ab1bfcf1f | |||
| 99f8dbd278 | |||
| 3af0bc2c33 | |||
| b8c4ce932c | |||
| 0a3a3edf77 | |||
| 71376229f6 | |||
| c9dde419a2 | |||
| 2fc01a02db | |||
| f02d2344fc | |||
| 509311ac19 | |||
| 47e7c4f1c1 | |||
| c9d3ba900e | |||
| 74a3d11aea | |||
| 897abc114e | |||
| 3fff3003f2 | |||
| db5c93f96d |
@@ -143,7 +143,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
|
||||
skip-binary: aiohttp;multidict;yarl
|
||||
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements.txt"
|
||||
|
||||
@@ -42,7 +42,7 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
mac = format_mac(user_input[CONF_ADDRESS])
|
||||
mac = user_input[CONF_ADDRESS]
|
||||
try:
|
||||
is_new_style_scale = await is_new_scale(mac)
|
||||
except AcaiaDeviceNotFound:
|
||||
@@ -53,12 +53,12 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
except AcaiaUnknownDevice:
|
||||
return self.async_abort(reason="unsupported_device")
|
||||
else:
|
||||
await self.async_set_unique_id(mac)
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_devices[user_input[CONF_ADDRESS]],
|
||||
title=self._discovered_devices[mac],
|
||||
data={
|
||||
CONF_ADDRESS: mac,
|
||||
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
|
||||
@@ -99,10 +99,10 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a discovered Bluetooth device."""
|
||||
|
||||
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
|
||||
self._discovered[CONF_ADDRESS] = discovery_info.address
|
||||
self._discovered[CONF_NAME] = discovery_info.name
|
||||
|
||||
await self.async_set_unique_id(mac)
|
||||
await self.async_set_unique_id(format_mac(discovery_info.address))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_BLUETOOTH,
|
||||
DeviceInfo,
|
||||
format_mac,
|
||||
)
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -25,13 +29,15 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._scale = coordinator.scale
|
||||
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
|
||||
formatted_mac = format_mac(self._scale.mac)
|
||||
self._attr_unique_id = f"{formatted_mac}_{entity_description.key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._scale.mac)},
|
||||
identifiers={(DOMAIN, formatted_mac)},
|
||||
manufacturer="Acaia",
|
||||
model=self._scale.model,
|
||||
suggested_area="Kitchen",
|
||||
connections={(CONNECTION_BLUETOOTH, self._scale.mac)},
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioacaia"],
|
||||
"requirements": ["aioacaia==0.1.9"]
|
||||
"requirements": ["aioacaia==0.1.10"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyatv", "srptools"],
|
||||
"requirements": ["pyatv==0.15.1"],
|
||||
"requirements": ["pyatv==0.16.0"],
|
||||
"zeroconf": [
|
||||
"_mediaremotetv._tcp.local.",
|
||||
"_companion-link._tcp.local.",
|
||||
|
||||
@@ -5,12 +5,17 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData
|
||||
from APsystemsEZ1 import (
|
||||
APsystemsEZ1M,
|
||||
InverterReturnedError,
|
||||
ReturnAlarmInfo,
|
||||
ReturnOutputData,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,6 +48,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
self.api.min_power = device_info.minPower
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
output_data = await self.api.get_output_data()
|
||||
alarm_info = await self.api.get_alarm_info()
|
||||
try:
|
||||
output_data = await self.api.get_output_data()
|
||||
alarm_info = await self.api.get_alarm_info()
|
||||
except InverterReturnedError:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="inverter_error"
|
||||
) from None
|
||||
return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info)
|
||||
|
||||
@@ -72,5 +72,10 @@
|
||||
"name": "Inverter status"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"inverter_error": {
|
||||
"message": "Inverter returned an error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,6 +1018,7 @@ class PipelineRun:
|
||||
"intent_input": intent_input,
|
||||
"conversation_id": conversation_id,
|
||||
"device_id": device_id,
|
||||
"prefer_local_intents": self.pipeline.prefer_local_intents,
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -1031,6 +1032,7 @@ class PipelineRun:
|
||||
language=self.pipeline.language,
|
||||
agent_id=self.intent_agent,
|
||||
)
|
||||
processed_locally = self.intent_agent == conversation.HOME_ASSISTANT_AGENT
|
||||
|
||||
conversation_result: conversation.ConversationResult | None = None
|
||||
if user_input.agent_id != conversation.HOME_ASSISTANT_AGENT:
|
||||
@@ -1040,7 +1042,7 @@ class PipelineRun:
|
||||
:= await conversation.async_handle_sentence_triggers(
|
||||
self.hass, user_input
|
||||
)
|
||||
):
|
||||
) is not None:
|
||||
# Sentence trigger matched
|
||||
trigger_response = intent.IntentResponse(
|
||||
self.pipeline.conversation_language
|
||||
@@ -1061,6 +1063,7 @@ class PipelineRun:
|
||||
response=intent_response,
|
||||
conversation_id=user_input.conversation_id,
|
||||
)
|
||||
processed_locally = True
|
||||
|
||||
if conversation_result is None:
|
||||
# Fall back to pipeline conversation agent
|
||||
@@ -1085,7 +1088,10 @@ class PipelineRun:
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.INTENT_END,
|
||||
{"intent_output": conversation_result.as_dict()},
|
||||
{
|
||||
"processed_locally": processed_locally,
|
||||
"intent_output": conversation_result.as_dict(),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -27,9 +27,18 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_US
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
|
||||
from .const import (
|
||||
CONF_ALLOWED_REGIONS,
|
||||
CONF_CAPTCHA_REGIONS,
|
||||
CONF_CAPTCHA_TOKEN,
|
||||
CONF_CAPTCHA_URL,
|
||||
CONF_GCID,
|
||||
CONF_READ_ONLY,
|
||||
CONF_REFRESH_TOKEN,
|
||||
)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -41,7 +50,14 @@ DATA_SCHEMA = vol.Schema(
|
||||
translation_key="regions",
|
||||
)
|
||||
),
|
||||
}
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
CAPTCHA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CAPTCHA_TOKEN): str,
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +70,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
get_region_from_name(data[CONF_REGION]),
|
||||
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
|
||||
verify=get_default_context(),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -79,15 +97,17 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
_existing_entry_data: Mapping[str, Any] | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
errors: dict[str, str] = self.data.pop("errors", {})
|
||||
|
||||
if user_input is not None:
|
||||
if user_input is not None and not errors:
|
||||
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
|
||||
await self.async_set_unique_id(unique_id)
|
||||
|
||||
@@ -96,22 +116,35 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Store user input for later use
|
||||
self.data.update(user_input)
|
||||
|
||||
# North America and Rest of World require captcha token
|
||||
if (
|
||||
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
|
||||
and CONF_CAPTCHA_TOKEN not in self.data
|
||||
):
|
||||
return await self.async_step_captcha()
|
||||
|
||||
info = None
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
entry_data = {
|
||||
**user_input,
|
||||
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
|
||||
CONF_GCID: info.get(CONF_GCID),
|
||||
}
|
||||
info = await validate_input(self.hass, self.data)
|
||||
except MissingCaptcha:
|
||||
errors["base"] = "missing_captcha"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
finally:
|
||||
self.data.pop(CONF_CAPTCHA_TOKEN, None)
|
||||
|
||||
if info:
|
||||
entry_data = {
|
||||
**self.data,
|
||||
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
|
||||
CONF_GCID: info.get(CONF_GCID),
|
||||
}
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=entry_data
|
||||
@@ -128,7 +161,7 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
DATA_SCHEMA,
|
||||
self._existing_entry_data,
|
||||
self._existing_entry_data or self.data,
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
@@ -147,6 +180,22 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._existing_entry_data = self._get_reconfigure_entry().data
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_captcha(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show captcha form."""
|
||||
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
|
||||
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
|
||||
return await self.async_step_user(self.data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="captcha",
|
||||
data_schema=CAPTCHA_SCHEMA,
|
||||
description_placeholders={
|
||||
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
||||
@@ -8,10 +8,15 @@ ATTR_DIRECTION = "direction"
|
||||
ATTR_VIN = "vin"
|
||||
|
||||
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
|
||||
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
|
||||
CONF_READ_ONLY = "read_only"
|
||||
CONF_ACCOUNT = "account"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
CONF_GCID = "gcid"
|
||||
CONF_CAPTCHA_TOKEN = "captcha_token"
|
||||
CONF_CAPTCHA_URL = (
|
||||
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
|
||||
)
|
||||
|
||||
DATA_HASS_CONFIG = "hass_config"
|
||||
|
||||
|
||||
@@ -84,11 +84,6 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
if self.account.refresh_token != old_refresh_token:
|
||||
self._update_config_entry_refresh_token(self.account.refresh_token)
|
||||
_LOGGER.debug(
|
||||
"bimmer_connected: refresh token %s > %s",
|
||||
old_refresh_token,
|
||||
self.account.refresh_token,
|
||||
)
|
||||
|
||||
def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
|
||||
"""Update or delete the refresh_token in the Config Entry."""
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer-connected[china]==0.16.4"]
|
||||
"requirements": ["bimmer-connected[china]==0.17.2"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"region": "ConnectedDrive Region"
|
||||
}
|
||||
},
|
||||
"captcha": {
|
||||
"title": "Are you a robot?",
|
||||
"description": "A captcha is required for BMW login. Visit the external website to complete the challenge and submit the form. Copy the resulting token into the field below.\n\n{captcha_url}\n\nNo data will be exposed outside of your Home Assistant instance.",
|
||||
"data": {
|
||||
"captcha_token": "Captcha token"
|
||||
},
|
||||
"data_description": {
|
||||
"captcha_token": "One-time token retrieved from the captcha challenge."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -742,6 +742,7 @@ class BrSensor(SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self._data: BrData | None = None
|
||||
self._measured = None
|
||||
self._attr_unique_id = (
|
||||
f"{coordinates[CONF_LATITUDE]:2.6f}{coordinates[CONF_LONGITUDE]:2.6f}"
|
||||
@@ -756,17 +757,29 @@ class BrSensor(SensorEntity):
|
||||
if description.key.startswith(PRECIPITATION_FORECAST):
|
||||
self._timeframe = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity being added to hass."""
|
||||
if self._data is None:
|
||||
return
|
||||
self._update()
|
||||
|
||||
@callback
|
||||
def data_updated(self, data: BrData):
|
||||
"""Update data."""
|
||||
if self._load_data(data.data) and self.hass:
|
||||
"""Handle data update."""
|
||||
self._data = data
|
||||
if not self.hass:
|
||||
return
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
"""Update sensor data."""
|
||||
_LOGGER.debug("Updating sensor %s", self.entity_id)
|
||||
if self._load_data(self._data.data):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _load_data(self, data): # noqa: C901
|
||||
"""Load the sensor with relevant data."""
|
||||
# Find sensor
|
||||
|
||||
# Check if we have a new measurement,
|
||||
# otherwise we do not have to update the sensor
|
||||
if self._measured == data.get(MEASURED):
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["hass_nabucasa"],
|
||||
"requirements": ["hass-nabucasa==0.85.0"],
|
||||
"requirements": ["hass-nabucasa==0.86.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -711,7 +711,7 @@ class DefaultAgent(ConversationEntity):
|
||||
for name_tuple in self._get_entity_name_tuples(exposed=False):
|
||||
self._unexposed_names_trie.insert(
|
||||
name_tuple[0].lower(),
|
||||
TextSlotValue.from_tuple(name_tuple),
|
||||
TextSlotValue.from_tuple(name_tuple, allow_template=False),
|
||||
)
|
||||
|
||||
# Build filtered slot list
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.0.4", "home-assistant-intents==2024.11.27"]
|
||||
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.4"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout
|
||||
from pydeako.discover import DeakoDiscoverer
|
||||
from pydeako import Deako, DeakoDiscoverer, FindDevicesError
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -30,12 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> boo
|
||||
await connection.connect()
|
||||
try:
|
||||
await connection.find_devices()
|
||||
except DeviceListTimeout as exc: # device list never received
|
||||
_LOGGER.warning("Device not responding to device list")
|
||||
await connection.disconnect()
|
||||
raise ConfigEntryNotReady(exc) from exc
|
||||
except FindDevicesTimeout as exc: # total devices expected not received
|
||||
_LOGGER.warning("Device not responding to device requests")
|
||||
except FindDevicesError as exc:
|
||||
_LOGGER.warning("Error finding devices: %s", exc)
|
||||
await connection.disconnect()
|
||||
raise ConfigEntryNotReady(exc) from exc
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Config flow for deako."""
|
||||
|
||||
from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException
|
||||
from pydeako import DeakoDiscoverer, DevicesNotFoundException
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydeako.deako import Deako
|
||||
from pydeako import Deako
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/deako",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydeako"],
|
||||
"requirements": ["pydeako==0.5.4"],
|
||||
"requirements": ["pydeako==0.6.0"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_deako._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.0.0"],
|
||||
"requirements": ["denonavr==1.0.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
||||
@@ -99,8 +99,8 @@ class EcovacsController:
|
||||
for device_config in devices.not_supported:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
'Device "%s" not supported. Please add support for it to '
|
||||
"https://github.com/DeebotUniverse/client.py: %s"
|
||||
'Device "%s" not supported. More information at '
|
||||
"https://github.com/DeebotUniverse/client.py/issues/612: %s"
|
||||
),
|
||||
device_config["deviceName"],
|
||||
device_config,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==9.0.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ def check_local_version_supported(api_version: str | None) -> bool:
|
||||
class DirectPanel(PanelEntry):
|
||||
"""Helper class for wrapping a directly accessed Elmax Panel."""
|
||||
|
||||
def __init__(self, panel_uri):
|
||||
def __init__(self, panel_uri) -> None:
|
||||
"""Construct the object."""
|
||||
super().__init__(panel_uri, True, {})
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_direct(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle the direct setup step."""
|
||||
self._selected_mode = CONF_ELMAX_MODE_CLOUD
|
||||
self._selected_mode = CONF_ELMAX_MODE_DIRECT
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id=CONF_ELMAX_MODE_DIRECT,
|
||||
|
||||
@@ -121,13 +121,13 @@ class ElmaxCover(ElmaxEntity, CoverEntity):
|
||||
else:
|
||||
_LOGGER.debug("Ignoring stop request as the cover is IDLE")
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self.coordinator.http_client.execute_command(
|
||||
endpoint_id=self._device.endpoint_id, command=CoverCommand.UP
|
||||
)
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
await self.coordinator.http_client.execute_command(
|
||||
endpoint_id=self._device.endpoint_id, command=CoverCommand.DOWN
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/elmax",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["elmax_api"],
|
||||
"requirements": ["elmax-api==0.0.6.1"],
|
||||
"requirements": ["elmax-api==0.0.6.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_elmax-ssl._tcp.local."
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==27.0.2",
|
||||
"aioesphomeapi==28.0.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==1.1.0"
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.assist_pipeline.select import (
|
||||
)
|
||||
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import restore_state
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -100,7 +101,9 @@ class EsphomeAssistSatelliteWakeWordSelect(
|
||||
"""Wake word selector for esphome devices."""
|
||||
|
||||
entity_description = SelectEntityDescription(
|
||||
key="wake_word", translation_key="wake_word"
|
||||
key="wake_word",
|
||||
translation_key="wake_word",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
_attr_should_poll = False
|
||||
_attr_current_option: str | None = None
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ezviz",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["paho_mqtt", "pyezviz"],
|
||||
"requirements": ["pyezviz==0.2.1.2"]
|
||||
"requirements": ["pyezviz==0.2.2.3"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20241127.0"]
|
||||
"requirements": ["home-assistant-frontend==20241127.6"]
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"message": "`{filename}` is not an image"
|
||||
},
|
||||
"missing_upload_permission": {
|
||||
"message": "Home Assistnt was not granted permission to upload to Google Photos"
|
||||
"message": "Home Assistant was not granted permission to upload to Google Photos"
|
||||
},
|
||||
"upload_error": {
|
||||
"message": "Failed to upload content: {message}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
@@ -39,8 +39,10 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str | None]:
|
||||
else:
|
||||
result["status"] = TodoItemStatus.NEEDS_ACTION
|
||||
if (due := item.due) is not None:
|
||||
# due API field is a timestamp string, but with only date resolution
|
||||
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
||||
# due API field is a timestamp string, but with only date resolution.
|
||||
# The time portion of the date is always discarded by the API, so we
|
||||
# always set to UTC.
|
||||
result["due"] = dt_util.start_of_local_day(due).replace(tzinfo=UTC).isoformat()
|
||||
else:
|
||||
result["due"] = None
|
||||
result["notes"] = item.description
|
||||
@@ -51,6 +53,8 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
||||
"""Convert tasks API items into a TodoItem."""
|
||||
due: date | None = None
|
||||
if (due_str := item.get("due")) is not None:
|
||||
# Due dates are returned always in UTC so we only need to
|
||||
# parse the date portion which will be interpreted as a a local date.
|
||||
due = datetime.fromisoformat(due_str).date()
|
||||
return TodoItem(
|
||||
summary=item["title"],
|
||||
|
||||
@@ -174,7 +174,7 @@ def get_attribute_points(
|
||||
)
|
||||
|
||||
return {
|
||||
"level": min(round(user["stats"]["lvl"] / 2), 50),
|
||||
"level": min(floor(user["stats"]["lvl"] / 2), 50),
|
||||
"equipment": equipment,
|
||||
"class": class_bonus,
|
||||
"allocated": user["stats"][attribute],
|
||||
|
||||
@@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import websocket_api
|
||||
from .const import DOMAIN
|
||||
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
|
||||
from .helpers import entities_may_have_state_changes_after, has_states_before
|
||||
|
||||
CONF_ORDER = "use_include_order"
|
||||
|
||||
@@ -107,7 +107,10 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
no_attributes = "no_attributes" in request.query
|
||||
|
||||
if (
|
||||
(end_time and not has_recorder_run_after(hass, end_time))
|
||||
# has_states_before will return True if there are states older than
|
||||
# end_time. If it's false, we know there are no states in the
|
||||
# database up until end_time.
|
||||
(end_time and not has_states_before(hass, end_time))
|
||||
or not include_start_time_state
|
||||
and entity_ids
|
||||
and not entities_may_have_state_changes_after(
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections.abc import Iterable
|
||||
from datetime import datetime as dt
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import process_timestamp
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@@ -26,8 +25,10 @@ def entities_may_have_state_changes_after(
|
||||
return False
|
||||
|
||||
|
||||
def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool:
|
||||
"""Check if the recorder has any runs after a specific time."""
|
||||
return run_time >= process_timestamp(
|
||||
get_instance(hass).recorder_runs_manager.first.start
|
||||
)
|
||||
def has_states_before(hass: HomeAssistant, run_time: dt) -> bool:
|
||||
"""Check if the recorder has states as old or older than run_time.
|
||||
|
||||
Returns True if there may be such states.
|
||||
"""
|
||||
oldest_ts = get_instance(hass).states_manager.oldest_ts
|
||||
return oldest_ts is not None and run_time.timestamp() >= oldest_ts
|
||||
|
||||
@@ -39,7 +39,7 @@ from homeassistant.util.async_ import create_eager_task
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
|
||||
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
|
||||
from .helpers import entities_may_have_state_changes_after, has_states_before
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -142,7 +142,10 @@ async def ws_get_history_during_period(
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
||||
if (
|
||||
(end_time and not has_recorder_run_after(hass, end_time))
|
||||
# has_states_before will return True if there are states older than
|
||||
# end_time. If it's false, we know there are no states in the
|
||||
# database up until end_time.
|
||||
(end_time and not has_states_before(hass, end_time))
|
||||
or not include_start_time_state
|
||||
and entity_ids
|
||||
and not entities_may_have_state_changes_after(
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State
|
||||
@@ -14,6 +16,8 @@ from .helpers import async_calculate_period, floored_timestamp
|
||||
|
||||
MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryStatsState:
|
||||
@@ -186,8 +190,13 @@ class HistoryStats:
|
||||
current_state_matches = history_state.state in self._entity_states
|
||||
state_change_timestamp = history_state.last_changed
|
||||
|
||||
if state_change_timestamp > now_timestamp:
|
||||
if math.floor(state_change_timestamp) > now_timestamp:
|
||||
# Shouldn't count states that are in the future
|
||||
_LOGGER.debug(
|
||||
"Skipping future timestamp %s (now %s)",
|
||||
state_change_timestamp,
|
||||
now_timestamp,
|
||||
)
|
||||
continue
|
||||
|
||||
if previous_state_matches:
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.61", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.62", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -140,12 +140,12 @@ TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir80Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir100Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.SabbathProgramme",
|
||||
"Cooking.Oven.Program.Microwave90Watt",
|
||||
"Cooking.Oven.Program.Microwave180Watt",
|
||||
"Cooking.Oven.Program.Microwave360Watt",
|
||||
"Cooking.Oven.Program.Microwave600Watt",
|
||||
"Cooking.Oven.Program.Microwave900Watt",
|
||||
"Cooking.Oven.Program.Microwave1000Watt",
|
||||
"Cooking.Oven.Program.Microwave.90Watt",
|
||||
"Cooking.Oven.Program.Microwave.180Watt",
|
||||
"Cooking.Oven.Program.Microwave.360Watt",
|
||||
"Cooking.Oven.Program.Microwave.600Watt",
|
||||
"Cooking.Oven.Program.Microwave.900Watt",
|
||||
"Cooking.Oven.Program.Microwave.1000Watt",
|
||||
"Cooking.Oven.Program.Microwave.Max",
|
||||
"Cooking.Oven.Program.HeatingMode.WarmingDrawer",
|
||||
"LaundryCare.Washer.Program.Cotton",
|
||||
|
||||
@@ -114,7 +114,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
|
||||
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
|
||||
INVALID_END_CHARS = "-_"
|
||||
INVALID_END_CHARS = "-_ "
|
||||
MAX_VERSION_PART = 2**32 - 1
|
||||
|
||||
|
||||
@@ -424,20 +424,12 @@ def cleanup_name_for_homekit(name: str | None) -> str:
|
||||
|
||||
def temperature_to_homekit(temperature: float, unit: str) -> float:
|
||||
"""Convert temperature to Celsius for HomeKit."""
|
||||
return round(
|
||||
TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS), 1
|
||||
)
|
||||
return TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS)
|
||||
|
||||
|
||||
def temperature_to_states(temperature: float, unit: str) -> float:
|
||||
"""Convert temperature back from Celsius to Home Assistant unit."""
|
||||
return (
|
||||
round(
|
||||
TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit)
|
||||
* 2
|
||||
)
|
||||
/ 2
|
||||
)
|
||||
return TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit)
|
||||
|
||||
|
||||
def density_to_air_quality(density: float) -> int:
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.6"],
|
||||
"requirements": ["aiohomekit==3.2.7"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -64,4 +64,4 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity):
|
||||
or (brightness := self.coordinator.data.state.brightness) is None
|
||||
):
|
||||
return None
|
||||
return brightness_to_value((0, 100), brightness)
|
||||
return round(brightness_to_value((0, 100), brightness))
|
||||
|
||||
@@ -326,7 +326,8 @@ class HomeAssistantApplication(web.Application):
|
||||
protocol,
|
||||
writer,
|
||||
task,
|
||||
loop=self._loop,
|
||||
# loop will never be None when called from aiohttp
|
||||
loop=self._loop, # type: ignore[arg-type]
|
||||
client_max_size=self._client_max_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
|
||||
@@ -17,6 +18,15 @@ CACHE_HEADER = f"public, max-age={CACHE_TIME}"
|
||||
CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER}
|
||||
RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512)
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
# guess_type is soft-deprecated in 3.13
|
||||
# for paths and should only be used for
|
||||
# URLs. guess_file_type should be used
|
||||
# for paths instead.
|
||||
_GUESSER = CONTENT_TYPES.guess_file_type
|
||||
else:
|
||||
_GUESSER = CONTENT_TYPES.guess_type
|
||||
|
||||
|
||||
class CachingStaticResource(StaticResource):
|
||||
"""Static Resource handler that will add cache headers."""
|
||||
@@ -37,9 +47,7 @@ class CachingStaticResource(StaticResource):
|
||||
# Must be directory index; ignore caching
|
||||
return response
|
||||
file_path = response._path # noqa: SLF001
|
||||
response.content_type = (
|
||||
CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE
|
||||
)
|
||||
response.content_type = _GUESSER(file_path)[0] or FALLBACK_CONTENT_TYPE
|
||||
# Cache actual header after setter construction.
|
||||
content_type = response.headers[CONTENT_TYPE]
|
||||
RESPONSE_CACHE[key] = (file_path, content_type)
|
||||
|
||||
@@ -332,7 +332,17 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
||||
raise UpdateFailed(
|
||||
f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}"
|
||||
)
|
||||
if not (count := len(message_ids := lines[0].split())):
|
||||
# Check we do have returned items.
|
||||
#
|
||||
# In rare cases, when no UID's are returned,
|
||||
# only the status line is returned, and not an empty line.
|
||||
# See: https://github.com/home-assistant/core/issues/132042
|
||||
#
|
||||
# Strictly the RfC notes that 0 or more numbers should be returned
|
||||
# delimited by a space.
|
||||
#
|
||||
# See: https://datatracker.ietf.org/doc/html/rfc3501#section-7.2.5
|
||||
if len(lines) == 1 or not (count := len(message_ids := lines[0].split())):
|
||||
self._last_message_uid = None
|
||||
return 0
|
||||
last_message_uid = (
|
||||
|
||||
@@ -36,7 +36,6 @@ SENSOR_TYPES: tuple[JellyfinSensorEntityDescription, ...] = (
|
||||
key="watching",
|
||||
translation_key="watching",
|
||||
value_fn=_count_now_playing,
|
||||
native_unit_of_measurement="clients",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"watching": {
|
||||
"name": "Active clients"
|
||||
"name": "Active clients",
|
||||
"unit_of_measurement": "clients"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,13 +41,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass, client.start_websocket(), "knocki-websocket"
|
||||
)
|
||||
await client.start_websocket()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.client.close()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["knocki"],
|
||||
"requirements": ["knocki==0.3.5"]
|
||||
"requirements": ["knocki==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ from .const import (
|
||||
CONF_KNX_SECURE_USER_PASSWORD,
|
||||
CONF_KNX_STATE_UPDATER,
|
||||
CONF_KNX_TELEGRAM_LOG_SIZE,
|
||||
CONF_KNX_TUNNEL_ENDPOINT_IA,
|
||||
CONF_KNX_TUNNELING,
|
||||
CONF_KNX_TUNNELING_TCP,
|
||||
CONF_KNX_TUNNELING_TCP_SECURE,
|
||||
@@ -352,6 +353,7 @@ class KNXModule:
|
||||
if _conn_type == CONF_KNX_TUNNELING_TCP:
|
||||
return ConnectionConfig(
|
||||
connection_type=ConnectionType.TUNNELING_TCP,
|
||||
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
|
||||
gateway_ip=self.entry.data[CONF_HOST],
|
||||
gateway_port=self.entry.data[CONF_PORT],
|
||||
auto_reconnect=True,
|
||||
@@ -364,6 +366,7 @@ class KNXModule:
|
||||
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
|
||||
return ConnectionConfig(
|
||||
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
|
||||
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
|
||||
gateway_ip=self.entry.data[CONF_HOST],
|
||||
gateway_port=self.entry.data[CONF_PORT],
|
||||
secure_config=SecureConfig(
|
||||
|
||||
@@ -104,7 +104,7 @@ class KNXConfigEntryData(TypedDict, total=False):
|
||||
route_back: bool # not required
|
||||
host: str # only required for tunnelling
|
||||
port: int # only required for tunnelling
|
||||
tunnel_endpoint_ia: str | None
|
||||
tunnel_endpoint_ia: str | None # tunnelling only - not required (use get())
|
||||
# KNX secure
|
||||
user_id: int | None # not required
|
||||
user_password: str | None # not required
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.httpx_client import create_async_httpx_client
|
||||
|
||||
from .const import CONF_USE_BLUETOOTH, DOMAIN
|
||||
from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
|
||||
@@ -47,11 +47,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
|
||||
assert entry.unique_id
|
||||
serial = entry.unique_id
|
||||
|
||||
client = create_async_httpx_client(hass)
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
client=get_async_client(hass),
|
||||
client=client,
|
||||
)
|
||||
|
||||
# initialize local API
|
||||
@@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
local_client = LaMarzoccoLocalClient(
|
||||
host=host,
|
||||
local_bearer=entry.data[CONF_TOKEN],
|
||||
client=get_async_client(hass),
|
||||
client=client,
|
||||
)
|
||||
|
||||
# initialize Bluetooth
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from httpx import AsyncClient
|
||||
from pylamarzocco.client_cloud import LaMarzoccoCloudClient
|
||||
from pylamarzocco.client_local import LaMarzoccoLocalClient
|
||||
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
|
||||
@@ -37,7 +38,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.httpx_client import create_async_httpx_client
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -57,6 +58,8 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 2
|
||||
|
||||
_client: AsyncClient
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._config: dict[str, Any] = {}
|
||||
@@ -79,10 +82,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
**user_input,
|
||||
**self._discovered,
|
||||
}
|
||||
self._client = create_async_httpx_client(self.hass)
|
||||
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=data[CONF_USERNAME],
|
||||
password=data[CONF_PASSWORD],
|
||||
client=self._client,
|
||||
)
|
||||
try:
|
||||
self._fleet = await cloud_client.get_customer_fleet()
|
||||
@@ -163,7 +168,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# validate local connection if host is provided
|
||||
if user_input.get(CONF_HOST):
|
||||
if not await LaMarzoccoLocalClient.validate_connection(
|
||||
client=get_async_client(self.hass),
|
||||
client=self._client,
|
||||
host=user_input[CONF_HOST],
|
||||
token=selected_device.communication_key,
|
||||
):
|
||||
@@ -291,6 +296,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_ADDRESS: discovery_info.macaddress,
|
||||
}
|
||||
)
|
||||
self._async_abort_entries_match({CONF_ADDRESS: discovery_info.macaddress})
|
||||
|
||||
_LOGGER.debug(
|
||||
"Discovered La Marzocco machine %s through DHCP at address %s",
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"requirements": ["pylamarzocco==1.2.11"]
|
||||
"requirements": ["pylamarzocco==1.2.12"]
|
||||
}
|
||||
|
||||
@@ -67,8 +67,10 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"title": "Update Configuration",
|
||||
"use_bluetooth": "Use Bluetooth"
|
||||
},
|
||||
"data_description": {
|
||||
"use_bluetooth": "Should the integration try to use Bluetooth to control the machine?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["thinqconnect"],
|
||||
"requirements": ["thinqconnect==1.0.1"]
|
||||
"requirements": ["thinqconnect==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class MatterAdapter:
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.platform_handlers: dict[Platform, AddEntitiesCallback] = {}
|
||||
self.discovered_entities: set[str] = set()
|
||||
|
||||
def register_platform_handler(
|
||||
self, platform: Platform, add_entities: AddEntitiesCallback
|
||||
@@ -54,23 +55,19 @@ class MatterAdapter:
|
||||
|
||||
async def setup_nodes(self) -> None:
|
||||
"""Set up all existing nodes and subscribe to new nodes."""
|
||||
initialized_nodes: set[int] = set()
|
||||
for node in self.matter_client.get_nodes():
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def node_added_callback(event: EventType, node: MatterNode) -> None:
|
||||
"""Handle node added event."""
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def node_updated_callback(event: EventType, node: MatterNode) -> None:
|
||||
"""Handle node updated event."""
|
||||
if node.node_id in initialized_nodes:
|
||||
return
|
||||
if not node.available:
|
||||
return
|
||||
initialized_nodes.add(node.node_id)
|
||||
# We always run the discovery logic again,
|
||||
# because the firmware version could have been changed or features added.
|
||||
self._setup_node(node)
|
||||
|
||||
def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
|
||||
@@ -237,11 +234,20 @@ class MatterAdapter:
|
||||
self._create_device_registry(endpoint)
|
||||
# run platform discovery from device type instances
|
||||
for entity_info in async_discover_entities(endpoint):
|
||||
discovery_key = (
|
||||
f"{entity_info.platform}_{endpoint.node.node_id}_{endpoint.endpoint_id}_"
|
||||
f"{entity_info.primary_attribute.cluster_id}_"
|
||||
f"{entity_info.primary_attribute.attribute_id}_"
|
||||
f"{entity_info.entity_description.key}"
|
||||
)
|
||||
if discovery_key in self.discovered_entities:
|
||||
continue
|
||||
LOGGER.debug(
|
||||
"Creating %s entity for %s",
|
||||
entity_info.platform,
|
||||
entity_info.primary_attribute,
|
||||
)
|
||||
self.discovered_entities.add(discovery_key)
|
||||
new_entity = entity_info.entity_class(
|
||||
self.matter_client, endpoint, entity_info
|
||||
)
|
||||
|
||||
@@ -159,6 +159,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
|
||||
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
|
||||
@@ -69,6 +69,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterCommandButton,
|
||||
required_attributes=(clusters.Identify.Attributes.AcceptedCommandList,),
|
||||
value_contains=clusters.Identify.Commands.Identify.command_id,
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BUTTON,
|
||||
|
||||
@@ -13,3 +13,5 @@ LOGGER = logging.getLogger(__package__)
|
||||
# prefixes to identify device identifier id types
|
||||
ID_TYPE_DEVICE_ID = "deviceid"
|
||||
ID_TYPE_SERIAL = "serial"
|
||||
|
||||
FEATUREMAP_ATTRIBUTE_ID = 65532
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.core import callback
|
||||
from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS
|
||||
from .button import DISCOVERY_SCHEMAS as BUTTON_SCHEMAS
|
||||
from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS
|
||||
from .const import FEATUREMAP_ATTRIBUTE_ID
|
||||
from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS
|
||||
from .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS
|
||||
from .fan import DISCOVERY_SCHEMAS as FAN_SCHEMAS
|
||||
@@ -121,12 +122,24 @@ def async_discover_entities(
|
||||
continue
|
||||
|
||||
# check for required value in (primary) attribute
|
||||
primary_attribute = schema.required_attributes[0]
|
||||
primary_value = endpoint.get_attribute_value(None, primary_attribute)
|
||||
if schema.value_contains is not None and (
|
||||
(primary_attribute := next((x for x in schema.required_attributes), None))
|
||||
is None
|
||||
or (value := endpoint.get_attribute_value(None, primary_attribute)) is None
|
||||
or not isinstance(value, list)
|
||||
or schema.value_contains not in value
|
||||
isinstance(primary_value, list)
|
||||
and schema.value_contains not in primary_value
|
||||
):
|
||||
continue
|
||||
|
||||
# check for required value in cluster featuremap
|
||||
if schema.featuremap_contains is not None and (
|
||||
not bool(
|
||||
int(
|
||||
endpoint.get_attribute_value(
|
||||
primary_attribute.cluster_id, FEATUREMAP_ATTRIBUTE_ID
|
||||
)
|
||||
)
|
||||
& schema.featuremap_contains
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -147,6 +160,7 @@ def async_discover_entities(
|
||||
attributes_to_watch=attributes_to_watch,
|
||||
entity_description=schema.entity_description,
|
||||
entity_class=schema.entity_class,
|
||||
discovery_schema=schema,
|
||||
)
|
||||
|
||||
# prevent re-discovery of the primary attribute if not allowed
|
||||
|
||||
@@ -16,9 +16,10 @@ from propcache import cached_property
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.typing import UndefinedType
|
||||
|
||||
from .const import DOMAIN, ID_TYPE_DEVICE_ID
|
||||
from .const import DOMAIN, FEATUREMAP_ATTRIBUTE_ID, ID_TYPE_DEVICE_ID
|
||||
from .helpers import get_device_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -140,6 +141,19 @@ class MatterEntity(Entity):
|
||||
node_filter=self._endpoint.node.node_id,
|
||||
)
|
||||
)
|
||||
# subscribe to FeatureMap attribute (as that can dynamically change)
|
||||
self._unsubscribes.append(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_featuremap_update,
|
||||
event_filter=EventType.ATTRIBUTE_UPDATED,
|
||||
node_filter=self._endpoint.node.node_id,
|
||||
attr_path_filter=create_attribute_path(
|
||||
endpoint=self._endpoint.endpoint_id,
|
||||
cluster_id=self._entity_info.primary_attribute.cluster_id,
|
||||
attribute_id=FEATUREMAP_ATTRIBUTE_ID,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str | UndefinedType | None:
|
||||
@@ -159,6 +173,29 @@ class MatterEntity(Entity):
|
||||
self._update_from_device()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _on_featuremap_update(
|
||||
self, event: EventType, data: tuple[int, str, int] | None
|
||||
) -> None:
|
||||
"""Handle FeatureMap attribute updates."""
|
||||
if data is None:
|
||||
return
|
||||
new_value = data[2]
|
||||
# handle edge case where a Feature is removed from a cluster
|
||||
if (
|
||||
self._entity_info.discovery_schema.featuremap_contains is not None
|
||||
and not bool(
|
||||
new_value & self._entity_info.discovery_schema.featuremap_contains
|
||||
)
|
||||
):
|
||||
# this entity is no longer supported by the device
|
||||
ent_reg = er.async_get(self.hass)
|
||||
ent_reg.async_remove(self.entity_id)
|
||||
|
||||
return
|
||||
# all other cases, just update the entity
|
||||
self._on_matter_event(event, data)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update data from Matter device."""
|
||||
|
||||
@@ -206,6 +206,5 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
entity_class=MatterLock,
|
||||
required_attributes=(clusters.DoorLock.Attributes.LockState,),
|
||||
optional_attributes=(clusters.DoorLock.Attributes.DoorState,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -51,6 +51,9 @@ class MatterEntityInfo:
|
||||
# entity class to use to instantiate the entity
|
||||
entity_class: type
|
||||
|
||||
# the original discovery schema used to create this entity
|
||||
discovery_schema: MatterDiscoverySchema
|
||||
|
||||
@property
|
||||
def primary_attribute(self) -> type[ClusterAttributeDescriptor]:
|
||||
"""Return Primary Attribute belonging to the entity."""
|
||||
@@ -113,6 +116,10 @@ class MatterDiscoverySchema:
|
||||
# NOTE: only works for list values
|
||||
value_contains: Any | None = None
|
||||
|
||||
# [optional] the primary attribute's cluster featuremap must contain this value
|
||||
# for example for the DoorSensor on a DoorLock Cluster
|
||||
featuremap_contains: int | None = None
|
||||
|
||||
# [optional] bool to specify if this primary value may be discovered
|
||||
# by multiple platforms
|
||||
allow_multi: bool = False
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.9.3"]
|
||||
"requirements": ["aiomealie==0.9.4"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2024.11.04"],
|
||||
"requirements": ["yt-dlp[default]==2024.12.03"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/met_eireann",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["meteireann"],
|
||||
"requirements": ["PyMetEireann==2021.8.0"]
|
||||
"requirements": ["PyMetEireann==2024.11.0"]
|
||||
}
|
||||
|
||||
@@ -158,8 +158,6 @@ async def async_modbus_setup(
|
||||
|
||||
async def async_stop_modbus(event: Event) -> None:
|
||||
"""Stop Modbus service."""
|
||||
|
||||
async_dispatcher_send(hass, SIGNAL_STOP_ENTITY)
|
||||
for client in hub_collect.values():
|
||||
await client.async_close()
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ def async_subscribe_internal(
|
||||
translation_placeholders={"topic": topic},
|
||||
) from exc
|
||||
client = mqtt_data.client
|
||||
if not client.connected and not mqtt_config_entry_enabled(hass):
|
||||
if not mqtt_config_entry_enabled(hass):
|
||||
raise HomeAssistantError(
|
||||
f"Cannot subscribe to topic '{topic}', MQTT is not enabled",
|
||||
translation_key="mqtt_not_setup_cannot_subscribe",
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["music_assistant"],
|
||||
"requirements": ["music-assistant-client==1.0.5"],
|
||||
"requirements": ["music-assistant-client==1.0.8"],
|
||||
"zeroconf": ["_mass._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
super().__init__(mass, player_id)
|
||||
self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
|
||||
self._attr_supported_features = SUPPORTED_FEATURES
|
||||
if PlayerFeature.SYNC in self.player.supported_features:
|
||||
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.GROUPING
|
||||
if PlayerFeature.VOLUME_MUTE in self.player.supported_features:
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -400,19 +400,19 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
player_ids: list[str] = []
|
||||
entity_registry = er.async_get(self.hass)
|
||||
for child_entity_id in group_members:
|
||||
# resolve HA entity_id to MA player_id
|
||||
if (hass_state := self.hass.states.get(child_entity_id)) is None:
|
||||
continue
|
||||
if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None:
|
||||
continue
|
||||
player_ids.append(mass_player_id)
|
||||
await self.mass.players.player_command_sync_many(self.player_id, player_ids)
|
||||
if not (entity_reg_entry := entity_registry.async_get(child_entity_id)):
|
||||
raise HomeAssistantError(f"Entity {child_entity_id} not found")
|
||||
# unique id is the MA player_id
|
||||
player_ids.append(entity_reg_entry.unique_id)
|
||||
await self.mass.players.player_command_group_many(self.player_id, player_ids)
|
||||
|
||||
@catch_musicassistant_error
|
||||
async def async_unjoin_player(self) -> None:
|
||||
"""Remove this player from any group."""
|
||||
await self.mass.players.player_command_unsync(self.player_id)
|
||||
await self.mass.players.player_command_ungroup(self.player_id)
|
||||
|
||||
@catch_musicassistant_error
|
||||
async def _async_handle_play_media(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["netdata"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["netdata==1.1.0"]
|
||||
"requirements": ["netdata==1.3.0"]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -70,7 +71,9 @@ async def async_setup_platform(
|
||||
port = config[CONF_PORT]
|
||||
resources = config[CONF_RESOURCES]
|
||||
|
||||
netdata = NetdataData(Netdata(host, port=port, timeout=20.0))
|
||||
netdata = NetdataData(
|
||||
Netdata(host, port=port, timeout=20.0, httpx_client=get_async_client(hass))
|
||||
)
|
||||
await netdata.async_update()
|
||||
|
||||
if netdata.api.metrics is None:
|
||||
|
||||
@@ -27,7 +27,9 @@ from .entity import NordpoolBaseEntity
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]]:
|
||||
def get_prices(
|
||||
data: DeliveryPeriodData,
|
||||
) -> dict[str, tuple[float | None, float, float | None]]:
|
||||
"""Return previous, current and next prices.
|
||||
|
||||
Output: {"SE3": (10.0, 10.5, 12.1)}
|
||||
@@ -39,6 +41,7 @@ def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]
|
||||
previous_time = current_time - timedelta(hours=1)
|
||||
next_time = current_time + timedelta(hours=1)
|
||||
price_data = data.entries
|
||||
LOGGER.debug("Price data: %s", price_data)
|
||||
for entry in price_data:
|
||||
if entry.start <= current_time <= entry.end:
|
||||
current_price_entries = entry.entry
|
||||
@@ -46,10 +49,20 @@ def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]
|
||||
last_price_entries = entry.entry
|
||||
if entry.start <= next_time <= entry.end:
|
||||
next_price_entries = entry.entry
|
||||
LOGGER.debug(
|
||||
"Last price %s, current price %s, next price %s",
|
||||
last_price_entries,
|
||||
current_price_entries,
|
||||
next_price_entries,
|
||||
)
|
||||
|
||||
result = {}
|
||||
for area, price in current_price_entries.items():
|
||||
result[area] = (last_price_entries[area], price, next_price_entries[area])
|
||||
result[area] = (
|
||||
last_price_entries.get(area),
|
||||
price,
|
||||
next_price_entries.get(area),
|
||||
)
|
||||
LOGGER.debug("Prices: %s", result)
|
||||
return result
|
||||
|
||||
@@ -90,7 +103,7 @@ class NordpoolDefaultSensorEntityDescription(SensorEntityDescription):
|
||||
class NordpoolPricesSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Nord Pool prices sensor entity."""
|
||||
|
||||
value_fn: Callable[[tuple[float, float, float]], float | None]
|
||||
value_fn: Callable[[tuple[float | None, float, float | None]], float | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -136,13 +149,13 @@ PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
|
||||
NordpoolPricesSensorEntityDescription(
|
||||
key="last_price",
|
||||
translation_key="last_price",
|
||||
value_fn=lambda data: data[0] / 1000,
|
||||
value_fn=lambda data: data[0] / 1000 if data[0] else None,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
NordpoolPricesSensorEntityDescription(
|
||||
key="next_price",
|
||||
translation_key="next_price",
|
||||
value_fn=lambda data: data[2] / 1000,
|
||||
value_fn=lambda data: data[2] / 1000 if data[2] else None,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -384,6 +384,18 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
):
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
if (translation_key := self._unit_of_measurement_translation_key) and (
|
||||
unit_of_measurement
|
||||
:= self.platform.default_language_platform_translations.get(translation_key)
|
||||
):
|
||||
if native_unit_of_measurement is not None:
|
||||
raise ValueError(
|
||||
f"Number entity {type(self)} from integration '{self.platform.platform_name}' "
|
||||
f"has a translation key for unit_of_measurement '{unit_of_measurement}', "
|
||||
f"but also has a native_unit_of_measurement '{native_unit_of_measurement}'"
|
||||
)
|
||||
return unit_of_measurement
|
||||
|
||||
return native_unit_of_measurement
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -480,7 +480,13 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
|
||||
NumberDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT},
|
||||
NumberDeviceClass.POWER: {
|
||||
UnitOfPower.WATT,
|
||||
UnitOfPower.KILO_WATT,
|
||||
UnitOfPower.MEGA_WATT,
|
||||
UnitOfPower.GIGA_WATT,
|
||||
UnitOfPower.TERA_WATT,
|
||||
},
|
||||
NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
|
||||
NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
|
||||
NumberDeviceClass.PRESSURE: set(UnitOfPressure),
|
||||
|
||||
@@ -18,7 +18,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="ads_blocked_today",
|
||||
translation_key="ads_blocked_today",
|
||||
native_unit_of_measurement="ads",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="ads_percentage_today",
|
||||
@@ -28,38 +27,20 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="clients_ever_seen",
|
||||
translation_key="clients_ever_seen",
|
||||
native_unit_of_measurement="clients",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="dns_queries_today",
|
||||
translation_key="dns_queries_today",
|
||||
native_unit_of_measurement="queries",
|
||||
key="dns_queries_today", translation_key="dns_queries_today"
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="domains_being_blocked",
|
||||
translation_key="domains_being_blocked",
|
||||
native_unit_of_measurement="domains",
|
||||
),
|
||||
SensorEntityDescription(key="queries_cached", translation_key="queries_cached"),
|
||||
SensorEntityDescription(
|
||||
key="queries_cached",
|
||||
translation_key="queries_cached",
|
||||
native_unit_of_measurement="queries",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="queries_forwarded",
|
||||
translation_key="queries_forwarded",
|
||||
native_unit_of_measurement="queries",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="unique_clients",
|
||||
translation_key="unique_clients",
|
||||
native_unit_of_measurement="clients",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="unique_domains",
|
||||
translation_key="unique_domains",
|
||||
native_unit_of_measurement="domains",
|
||||
key="queries_forwarded", translation_key="queries_forwarded"
|
||||
),
|
||||
SensorEntityDescription(key="unique_clients", translation_key="unique_clients"),
|
||||
SensorEntityDescription(key="unique_domains", translation_key="unique_domains"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -41,31 +41,39 @@
|
||||
},
|
||||
"sensor": {
|
||||
"ads_blocked_today": {
|
||||
"name": "Ads blocked today"
|
||||
"name": "Ads blocked today",
|
||||
"unit_of_measurement": "ads"
|
||||
},
|
||||
"ads_percentage_today": {
|
||||
"name": "Ads percentage blocked today"
|
||||
},
|
||||
"clients_ever_seen": {
|
||||
"name": "Seen clients"
|
||||
"name": "Seen clients",
|
||||
"unit_of_measurement": "clients"
|
||||
},
|
||||
"dns_queries_today": {
|
||||
"name": "DNS queries today"
|
||||
"name": "DNS queries today",
|
||||
"unit_of_measurement": "queries"
|
||||
},
|
||||
"domains_being_blocked": {
|
||||
"name": "Domains blocked"
|
||||
"name": "Domains blocked",
|
||||
"unit_of_measurement": "domains"
|
||||
},
|
||||
"queries_cached": {
|
||||
"name": "DNS queries cached"
|
||||
"name": "DNS queries cached",
|
||||
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::dns_queries_today::unit_of_measurement%]"
|
||||
},
|
||||
"queries_forwarded": {
|
||||
"name": "DNS queries forwarded"
|
||||
"name": "DNS queries forwarded",
|
||||
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::dns_queries_today::unit_of_measurement%]"
|
||||
},
|
||||
"unique_clients": {
|
||||
"name": "DNS unique clients"
|
||||
"name": "DNS unique clients",
|
||||
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::clients_ever_seen::unit_of_measurement%]"
|
||||
},
|
||||
"unique_domains": {
|
||||
"name": "DNS unique domains"
|
||||
"name": "DNS unique domains",
|
||||
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::domains_being_blocked::unit_of_measurement%]"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
||||
@@ -78,19 +78,18 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._attr_unique_id = f"{device_id}-climate"
|
||||
|
||||
self._devices = coordinator.data.devices
|
||||
self._gateway = coordinator.data.gateway
|
||||
gateway_id: str = self._gateway["gateway_id"]
|
||||
self._gateway_data = self._devices[gateway_id]
|
||||
|
||||
self._location = device_id
|
||||
if (location := self.device.get("location")) is not None:
|
||||
self._location = location
|
||||
|
||||
self.cdr_gateway = coordinator.data.gateway
|
||||
gateway_id: str = coordinator.data.gateway["gateway_id"]
|
||||
self.gateway_data = coordinator.data.devices[gateway_id]
|
||||
# Determine supported features
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if (
|
||||
self.cdr_gateway["cooling_present"]
|
||||
and self.cdr_gateway["smile_name"] != "Adam"
|
||||
):
|
||||
if self._gateway["cooling_present"] and self._gateway["smile_name"] != "Adam":
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
@@ -116,10 +115,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
"""
|
||||
# When no cooling available, _previous_mode is always heating
|
||||
if (
|
||||
"regulation_modes" in self.gateway_data
|
||||
and "cooling" in self.gateway_data["regulation_modes"]
|
||||
"regulation_modes" in self._gateway_data
|
||||
and "cooling" in self._gateway_data["regulation_modes"]
|
||||
):
|
||||
mode = self.gateway_data["select_regulation_mode"]
|
||||
mode = self._gateway_data["select_regulation_mode"]
|
||||
if mode in ("cooling", "heating"):
|
||||
self._previous_mode = mode
|
||||
|
||||
@@ -166,17 +165,17 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Return a list of available HVACModes."""
|
||||
hvac_modes: list[HVACMode] = []
|
||||
if "regulation_modes" in self.gateway_data:
|
||||
if "regulation_modes" in self._gateway_data:
|
||||
hvac_modes.append(HVACMode.OFF)
|
||||
|
||||
if "available_schedules" in self.device:
|
||||
hvac_modes.append(HVACMode.AUTO)
|
||||
|
||||
if self.cdr_gateway["cooling_present"]:
|
||||
if "regulation_modes" in self.gateway_data:
|
||||
if self.gateway_data["select_regulation_mode"] == "cooling":
|
||||
if self._gateway["cooling_present"]:
|
||||
if "regulation_modes" in self._gateway_data:
|
||||
if self._gateway_data["select_regulation_mode"] == "cooling":
|
||||
hvac_modes.append(HVACMode.COOL)
|
||||
if self.gateway_data["select_regulation_mode"] == "heating":
|
||||
if self._gateway_data["select_regulation_mode"] == "heating":
|
||||
hvac_modes.append(HVACMode.HEAT)
|
||||
else:
|
||||
hvac_modes.append(HVACMode.HEAT_COOL)
|
||||
@@ -192,17 +191,21 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
self._previous_action_mode(self.coordinator)
|
||||
|
||||
# Adam provides the hvac_action for each thermostat
|
||||
if (control_state := self.device.get("control_state")) == "cooling":
|
||||
return HVACAction.COOLING
|
||||
if control_state == "heating":
|
||||
return HVACAction.HEATING
|
||||
if control_state == "preheating":
|
||||
return HVACAction.PREHEATING
|
||||
if control_state == "off":
|
||||
if self._gateway["smile_name"] == "Adam":
|
||||
if (control_state := self.device.get("control_state")) == "cooling":
|
||||
return HVACAction.COOLING
|
||||
if control_state == "heating":
|
||||
return HVACAction.HEATING
|
||||
if control_state == "preheating":
|
||||
return HVACAction.PREHEATING
|
||||
if control_state == "off":
|
||||
return HVACAction.IDLE
|
||||
|
||||
return HVACAction.IDLE
|
||||
|
||||
heater: str = self.coordinator.data.gateway["heater_id"]
|
||||
heater_data = self.coordinator.data.devices[heater]
|
||||
# Anna
|
||||
heater: str = self._gateway["heater_id"]
|
||||
heater_data = self._devices[heater]
|
||||
if heater_data["binary_sensors"]["heating_state"]:
|
||||
return HVACAction.HEATING
|
||||
if heater_data["binary_sensors"].get("cooling_state", False):
|
||||
|
||||
@@ -100,13 +100,11 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALL_TORRENTS,
|
||||
translation_key="all_torrents",
|
||||
native_unit_of_measurement="torrents",
|
||||
value_fn=lambda coordinator: count_torrents_in_states(coordinator, []),
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ACTIVE_TORRENTS,
|
||||
translation_key="active_torrents",
|
||||
native_unit_of_measurement="torrents",
|
||||
value_fn=lambda coordinator: count_torrents_in_states(
|
||||
coordinator, ["downloading", "uploading"]
|
||||
),
|
||||
@@ -114,7 +112,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_INACTIVE_TORRENTS,
|
||||
translation_key="inactive_torrents",
|
||||
native_unit_of_measurement="torrents",
|
||||
value_fn=lambda coordinator: count_torrents_in_states(
|
||||
coordinator, ["stalledDL", "stalledUP"]
|
||||
),
|
||||
@@ -122,7 +119,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_PAUSED_TORRENTS,
|
||||
translation_key="paused_torrents",
|
||||
native_unit_of_measurement="torrents",
|
||||
value_fn=lambda coordinator: count_torrents_in_states(
|
||||
coordinator, ["pausedDL", "pausedUP"]
|
||||
),
|
||||
|
||||
@@ -36,16 +36,20 @@
|
||||
}
|
||||
},
|
||||
"active_torrents": {
|
||||
"name": "Active torrents"
|
||||
"name": "Active torrents",
|
||||
"unit_of_measurement": "torrents"
|
||||
},
|
||||
"inactive_torrents": {
|
||||
"name": "Inactive torrents"
|
||||
"name": "Inactive torrents",
|
||||
"unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]"
|
||||
},
|
||||
"paused_torrents": {
|
||||
"name": "Paused torrents"
|
||||
"name": "Paused torrents",
|
||||
"unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]"
|
||||
},
|
||||
"all_torrents": {
|
||||
"name": "All torrents"
|
||||
"name": "All torrents",
|
||||
"unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
"title": "[%key:component::rainbird::config::step::user::title%]",
|
||||
"data": {
|
||||
"duration": "Default irrigation time in minutes"
|
||||
},
|
||||
"data_description": {
|
||||
"duration": "The default duration the sprinkler will run when turned on."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@ class Recorder(threading.Thread):
|
||||
self.schema_version = schema_status.current_version
|
||||
|
||||
# Do non-live data migration
|
||||
migration.migrate_data_non_live(self, self.get_session, schema_status)
|
||||
self._migrate_data_offline(schema_status)
|
||||
|
||||
# Non-live migration is now completed, remaining steps are live
|
||||
self.migration_is_live = True
|
||||
@@ -916,6 +916,13 @@ class Recorder(threading.Thread):
|
||||
|
||||
return False
|
||||
|
||||
def _migrate_data_offline(
|
||||
self, schema_status: migration.SchemaValidationStatus
|
||||
) -> None:
|
||||
"""Migrate data."""
|
||||
with self.hass.timeout.freeze(DOMAIN):
|
||||
migration.migrate_data_non_live(self, self.get_session, schema_status)
|
||||
|
||||
def _migrate_schema_offline(
|
||||
self, schema_status: migration.SchemaValidationStatus
|
||||
) -> tuple[bool, migration.SchemaValidationStatus]:
|
||||
@@ -1424,6 +1431,7 @@ class Recorder(threading.Thread):
|
||||
with session_scope(session=self.get_session()) as session:
|
||||
end_incomplete_runs(session, self.recorder_runs_manager.recording_start)
|
||||
self.recorder_runs_manager.start(session)
|
||||
self.states_manager.load_from_db(session)
|
||||
|
||||
self._open_event_session()
|
||||
|
||||
|
||||
@@ -162,14 +162,14 @@ class Unused(CHAR):
|
||||
"""An unused column type that behaves like a string."""
|
||||
|
||||
|
||||
@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call]
|
||||
@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call]
|
||||
@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite")
|
||||
@compiles(Unused, "mysql", "mariadb", "sqlite")
|
||||
def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str:
|
||||
"""Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite."""
|
||||
return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite)
|
||||
|
||||
|
||||
@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call]
|
||||
@compiles(Unused, "postgresql")
|
||||
def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str:
|
||||
"""Compile Unused as CHAR(1) on postgresql."""
|
||||
return "CHAR(1)" # Uses 1 byte
|
||||
|
||||
@@ -22,9 +22,9 @@ from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||
from homeassistant.helpers.recorder import get_instance
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from ..db_schema import RecorderRuns, StateAttributes, States
|
||||
from ..db_schema import StateAttributes, States
|
||||
from ..filters import Filters
|
||||
from ..models import process_timestamp, process_timestamp_to_utc_isoformat
|
||||
from ..models import process_timestamp_to_utc_isoformat
|
||||
from ..models.legacy import LegacyLazyState, legacy_row_to_compressed_state
|
||||
from ..util import execute_stmt_lambda_element, session_scope
|
||||
from .const import (
|
||||
@@ -436,7 +436,7 @@ def get_last_state_changes(
|
||||
|
||||
|
||||
def _get_states_for_entities_stmt(
|
||||
run_start: datetime,
|
||||
run_start_ts: float,
|
||||
utc_point_in_time: datetime,
|
||||
entity_ids: list[str],
|
||||
no_attributes: bool,
|
||||
@@ -447,7 +447,6 @@ def _get_states_for_entities_stmt(
|
||||
)
|
||||
# We got an include-list of entities, accelerate the query by filtering already
|
||||
# in the inner query.
|
||||
run_start_ts = process_timestamp(run_start).timestamp()
|
||||
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
|
||||
stmt += lambda q: q.join(
|
||||
(
|
||||
@@ -483,7 +482,7 @@ def _get_rows_with_session(
|
||||
session: Session,
|
||||
utc_point_in_time: datetime,
|
||||
entity_ids: list[str],
|
||||
run: RecorderRuns | None = None,
|
||||
*,
|
||||
no_attributes: bool = False,
|
||||
) -> Iterable[Row]:
|
||||
"""Return the states at a specific point in time."""
|
||||
@@ -495,17 +494,16 @@ def _get_rows_with_session(
|
||||
),
|
||||
)
|
||||
|
||||
if run is None:
|
||||
run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time)
|
||||
oldest_ts = get_instance(hass).states_manager.oldest_ts
|
||||
|
||||
if run is None or process_timestamp(run.start) > utc_point_in_time:
|
||||
# History did not run before utc_point_in_time
|
||||
if oldest_ts is None or oldest_ts > utc_point_in_time.timestamp():
|
||||
# We don't have any states for the requested time
|
||||
return []
|
||||
|
||||
# We have more than one entity to look at so we need to do a query on states
|
||||
# since the last recorder run started.
|
||||
stmt = _get_states_for_entities_stmt(
|
||||
run.start, utc_point_in_time, entity_ids, no_attributes
|
||||
oldest_ts, utc_point_in_time, entity_ids, no_attributes
|
||||
)
|
||||
return execute_stmt_lambda_element(session, stmt)
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ from ..models import (
|
||||
LazyState,
|
||||
datetime_to_timestamp_or_none,
|
||||
extract_metadata_ids,
|
||||
process_timestamp,
|
||||
row_to_compressed_state,
|
||||
)
|
||||
from ..util import execute_stmt_lambda_element, session_scope
|
||||
@@ -246,9 +245,9 @@ def get_significant_states_with_session(
|
||||
if metadata_id is not None
|
||||
and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS
|
||||
]
|
||||
run_start_ts: float | None = None
|
||||
oldest_ts: float | None = None
|
||||
if include_start_time_state and not (
|
||||
run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time)
|
||||
oldest_ts := _get_oldest_possible_ts(hass, start_time)
|
||||
):
|
||||
include_start_time_state = False
|
||||
start_time_ts = dt_util.utc_to_timestamp(start_time)
|
||||
@@ -264,7 +263,7 @@ def get_significant_states_with_session(
|
||||
significant_changes_only,
|
||||
no_attributes,
|
||||
include_start_time_state,
|
||||
run_start_ts,
|
||||
oldest_ts,
|
||||
),
|
||||
track_on=[
|
||||
bool(single_metadata_id),
|
||||
@@ -411,9 +410,9 @@ def state_changes_during_period(
|
||||
entity_id_to_metadata_id: dict[str, int | None] = {
|
||||
entity_id: single_metadata_id
|
||||
}
|
||||
run_start_ts: float | None = None
|
||||
oldest_ts: float | None = None
|
||||
if include_start_time_state and not (
|
||||
run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time)
|
||||
oldest_ts := _get_oldest_possible_ts(hass, start_time)
|
||||
):
|
||||
include_start_time_state = False
|
||||
start_time_ts = dt_util.utc_to_timestamp(start_time)
|
||||
@@ -426,7 +425,7 @@ def state_changes_during_period(
|
||||
no_attributes,
|
||||
limit,
|
||||
include_start_time_state,
|
||||
run_start_ts,
|
||||
oldest_ts,
|
||||
has_last_reported,
|
||||
),
|
||||
track_on=[
|
||||
@@ -600,17 +599,17 @@ def _get_start_time_state_for_entities_stmt(
|
||||
)
|
||||
|
||||
|
||||
def _get_run_start_ts_for_utc_point_in_time(
|
||||
def _get_oldest_possible_ts(
|
||||
hass: HomeAssistant, utc_point_in_time: datetime
|
||||
) -> float | None:
|
||||
"""Return the start time of a run."""
|
||||
run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time)
|
||||
if (
|
||||
run is not None
|
||||
and (run_start := process_timestamp(run.start)) < utc_point_in_time
|
||||
):
|
||||
return run_start.timestamp()
|
||||
# History did not run before utc_point_in_time but we still
|
||||
"""Return the oldest possible timestamp.
|
||||
|
||||
Returns None if there are no states as old as utc_point_in_time.
|
||||
"""
|
||||
|
||||
oldest_ts = get_instance(hass).states_manager.oldest_ts
|
||||
if oldest_ts is not None and oldest_ts < utc_point_in_time.timestamp():
|
||||
return oldest_ts
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.31",
|
||||
"SQLAlchemy==2.0.36",
|
||||
"fnv-hash-fast==1.0.2",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
|
||||
@@ -123,6 +123,9 @@ def purge_old_data(
|
||||
_purge_old_entity_ids(instance, session)
|
||||
|
||||
_purge_old_recorder_runs(instance, session, purge_before)
|
||||
with session_scope(session=instance.get_session(), read_only=True) as session:
|
||||
instance.recorder_runs_manager.load_from_db(session)
|
||||
instance.states_manager.load_from_db(session)
|
||||
if repack:
|
||||
repack_database(instance)
|
||||
return True
|
||||
|
||||
@@ -637,6 +637,15 @@ def find_states_to_purge(
|
||||
)
|
||||
|
||||
|
||||
def find_oldest_state() -> StatementLambdaElement:
|
||||
"""Find the last_updated_ts of the oldest state."""
|
||||
return lambda_stmt(
|
||||
lambda: select(States.last_updated_ts).where(
|
||||
States.state_id.in_(select(func.min(States.state_id)))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def find_short_term_statistics_to_purge(
|
||||
purge_before: datetime, max_bind_vars: int
|
||||
) -> StatementLambdaElement:
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from sqlalchemy.engine.row import Row
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from ..db_schema import States
|
||||
from ..queries import find_oldest_state
|
||||
from ..util import execute_stmt_lambda_element
|
||||
|
||||
|
||||
class StatesManager:
|
||||
@@ -13,6 +21,12 @@ class StatesManager:
|
||||
self._pending: dict[str, States] = {}
|
||||
self._last_committed_id: dict[str, int] = {}
|
||||
self._last_reported: dict[int, float] = {}
|
||||
self._oldest_ts: float | None = None
|
||||
|
||||
@property
|
||||
def oldest_ts(self) -> float | None:
|
||||
"""Return the oldest timestamp."""
|
||||
return self._oldest_ts
|
||||
|
||||
def pop_pending(self, entity_id: str) -> States | None:
|
||||
"""Pop a pending state.
|
||||
@@ -44,6 +58,8 @@ class StatesManager:
|
||||
recorder thread.
|
||||
"""
|
||||
self._pending[entity_id] = state
|
||||
if self._oldest_ts is None:
|
||||
self._oldest_ts = state.last_updated_ts
|
||||
|
||||
def update_pending_last_reported(
|
||||
self, state_id: int, last_reported_timestamp: float
|
||||
@@ -74,6 +90,22 @@ class StatesManager:
|
||||
"""
|
||||
self._last_committed_id.clear()
|
||||
self._pending.clear()
|
||||
self._oldest_ts = None
|
||||
|
||||
def load_from_db(self, session: Session) -> None:
|
||||
"""Update the cache.
|
||||
|
||||
Must run in the recorder thread.
|
||||
"""
|
||||
result = cast(
|
||||
Sequence[Row[Any]],
|
||||
execute_stmt_lambda_element(session, find_oldest_state()),
|
||||
)
|
||||
if not result:
|
||||
ts = None
|
||||
else:
|
||||
ts = result[0].last_updated_ts
|
||||
self._oldest_ts = ts
|
||||
|
||||
def evict_purged_state_ids(self, purged_state_ids: set[int]) -> None:
|
||||
"""Evict purged states from the committed states.
|
||||
|
||||
@@ -120,8 +120,6 @@ class PurgeTask(RecorderTask):
|
||||
if purge.purge_old_data(
|
||||
instance, self.purge_before, self.repack, self.apply_filter
|
||||
):
|
||||
with instance.get_session() as session:
|
||||
instance.recorder_runs_manager.load_from_db(session)
|
||||
# We always need to do the db cleanups after a purge
|
||||
# is finished to ensure the WAL checkpoint and other
|
||||
# tasks happen after a vacuum.
|
||||
|
||||
@@ -902,7 +902,7 @@ def resolve_period(
|
||||
start_time = (start_time + timedelta(days=cal_offset * 366)).replace(
|
||||
month=1, day=1
|
||||
)
|
||||
end_time = (start_time + timedelta(days=365)).replace(day=1)
|
||||
end_time = (start_time + timedelta(days=366)).replace(day=1)
|
||||
|
||||
start_time = dt_util.as_utc(start_time)
|
||||
end_time = dt_util.as_utc(end_time)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/refoss",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["refoss-ha==1.2.4"]
|
||||
"requirements": ["refoss-ha==1.2.5"]
|
||||
}
|
||||
|
||||
@@ -176,14 +176,14 @@ class ReolinkPushBinarySensorEntity(ReolinkBinarySensorEntity):
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{self._host.webhook_id}_{self._channel}",
|
||||
f"{self._host.unique_id}_{self._channel}",
|
||||
self._async_handle_event,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{self._host.webhook_id}_all",
|
||||
f"{self._host.unique_id}_all",
|
||||
self._async_handle_event,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -536,6 +536,8 @@ class ReolinkHost:
|
||||
|
||||
async def renew(self) -> None:
|
||||
"""Renew the subscription of motion events (lease time is 15 minutes)."""
|
||||
await self._api.baichuan.check_subscribe_events()
|
||||
|
||||
if self._api.baichuan.events_active and self._api.subscribed(SubType.push):
|
||||
# TCP push active, unsubscribe from ONVIF push because not needed
|
||||
self.unregister_webhook()
|
||||
@@ -721,7 +723,7 @@ class ReolinkHost:
|
||||
self._hass, POLL_INTERVAL_NO_PUSH, self._poll_job
|
||||
)
|
||||
|
||||
self._signal_write_ha_state(None)
|
||||
self._signal_write_ha_state()
|
||||
|
||||
async def handle_webhook(
|
||||
self, hass: HomeAssistant, webhook_id: str, request: Request
|
||||
@@ -780,7 +782,7 @@ class ReolinkHost:
|
||||
"Could not poll motion state after losing connection during receiving ONVIF event"
|
||||
)
|
||||
return
|
||||
async_dispatcher_send(hass, f"{webhook_id}_all", {})
|
||||
self._signal_write_ha_state()
|
||||
return
|
||||
|
||||
message = data.decode("utf-8")
|
||||
@@ -793,14 +795,14 @@ class ReolinkHost:
|
||||
|
||||
self._signal_write_ha_state(channels)
|
||||
|
||||
def _signal_write_ha_state(self, channels: list[int] | None) -> None:
|
||||
def _signal_write_ha_state(self, channels: list[int] | None = None) -> None:
|
||||
"""Update the binary sensors with async_write_ha_state."""
|
||||
if channels is None:
|
||||
async_dispatcher_send(self._hass, f"{self.webhook_id}_all", {})
|
||||
async_dispatcher_send(self._hass, f"{self.unique_id}_all", {})
|
||||
return
|
||||
|
||||
for channel in channels:
|
||||
async_dispatcher_send(self._hass, f"{self.webhook_id}_{channel}", {})
|
||||
async_dispatcher_send(self._hass, f"{self.unique_id}_{channel}", {})
|
||||
|
||||
@property
|
||||
def event_connection(self) -> str:
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.11.3"]
|
||||
"requirements": ["reolink-aio==0.11.4"]
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"requirements": [
|
||||
"getmac==0.9.4",
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.7.0",
|
||||
"samsungtvws[async,encrypted]==2.7.2",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.41.0"
|
||||
],
|
||||
|
||||
@@ -504,17 +504,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return self.entity_description.suggested_unit_of_measurement
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def _unit_of_measurement_translation_key(self) -> str | None:
|
||||
"""Return translation key for unit of measurement."""
|
||||
if self.translation_key is None:
|
||||
return None
|
||||
platform = self.platform
|
||||
return (
|
||||
f"component.{platform.platform_name}.entity.{platform.domain}"
|
||||
f".{self.translation_key}.unit_of_measurement"
|
||||
)
|
||||
|
||||
@final
|
||||
@property
|
||||
@override
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user