mirror of
https://github.com/home-assistant/core.git
synced 2026-03-24 08:18:29 +01:00
Compare commits
109 Commits
2026.3.0b4
...
2026.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d5e89951 | ||
|
|
557d072a4d | ||
|
|
6c3917e927 | ||
|
|
e895c1b2fd | ||
|
|
dae971cd98 | ||
|
|
807df50eab | ||
|
|
aa05ff03b3 | ||
|
|
622b92682e | ||
|
|
a81146a227 | ||
|
|
530dcadf19 | ||
|
|
4aa67ddf22 | ||
|
|
8e95b19c4c | ||
|
|
5558b33600 | ||
|
|
0130ac6770 | ||
|
|
26d22e4d62 | ||
|
|
532bc02d66 | ||
|
|
893eac0e84 | ||
|
|
c1bd83c9c0 | ||
|
|
b3c27e9f93 | ||
|
|
92e237ade2 | ||
|
|
cbc573a6b1 | ||
|
|
0c059cfc27 | ||
|
|
143ce9d7b3 | ||
|
|
a6aa837d40 | ||
|
|
c58b4a0066 | ||
|
|
5155242ba7 | ||
|
|
085680f6bf | ||
|
|
98ecaaa6d2 | ||
|
|
5ad199fe16 | ||
|
|
413cb98424 | ||
|
|
b38c5bcaf2 | ||
|
|
fa85dfb3b5 | ||
|
|
f0c6a035db | ||
|
|
3f0c200e56 | ||
|
|
a2259ede28 | ||
|
|
24c2b6fe81 | ||
|
|
efc7350e6f | ||
|
|
5f525fc2a1 | ||
|
|
f619a3e7af | ||
|
|
4e43492342 | ||
|
|
39e70071d3 | ||
|
|
6da0936a66 | ||
|
|
5257702530 | ||
|
|
93da5be052 | ||
|
|
2c47e83342 | ||
|
|
e3c6a2184d | ||
|
|
0ba0829350 | ||
|
|
678048e681 | ||
|
|
743eeeae53 | ||
|
|
46555c6d9a | ||
|
|
dbaca0a723 | ||
|
|
9bb2959029 | ||
|
|
0304781fa9 | ||
|
|
e081d28aa4 | ||
|
|
34aa28c72f | ||
|
|
cfa2946db8 | ||
|
|
1b0779347c | ||
|
|
93a281e7af | ||
|
|
6b32e27fd3 | ||
|
|
79928a8c7c | ||
|
|
9146518e13 | ||
|
|
e9c5172f43 | ||
|
|
cce21ad4b9 | ||
|
|
10ec02ca3c | ||
|
|
bdf54491e5 | ||
|
|
0b05d34238 | ||
|
|
4c69a1c5f7 | ||
|
|
6f1f56dcaa | ||
|
|
d0b9991232 | ||
|
|
aacf39be8a | ||
|
|
bf055da82c | ||
|
|
0fb118bcd9 | ||
|
|
954ef7d1f5 | ||
|
|
b091299320 | ||
|
|
52483e18b2 | ||
|
|
57e8683ed7 | ||
|
|
67faace978 | ||
|
|
e4be64fcb1 | ||
|
|
f552b8221f | ||
|
|
55dc5392f9 | ||
|
|
5b93aeae38 | ||
|
|
33610bb1a1 | ||
|
|
6c3cebe413 | ||
|
|
5346895d9b | ||
|
|
05c3f08c6c | ||
|
|
1ce025733d | ||
|
|
1537ea86b8 | ||
|
|
ec137870fa | ||
|
|
816ee7f53e | ||
|
|
6e7eeec827 | ||
|
|
d100477a22 | ||
|
|
98ac6dd2c1 | ||
|
|
6b30969f60 | ||
|
|
e9a6b5d662 | ||
|
|
f95f3f9982 | ||
|
|
3f884a8cd1 | ||
|
|
10f284932e | ||
|
|
e1c4e6dc42 | ||
|
|
0976e7de4e | ||
|
|
ae1012b2f0 | ||
|
|
bb7c4faca5 | ||
|
|
0b1be61336 | ||
|
|
3ec44024a2 | ||
|
|
1200cc5779 | ||
|
|
d632931f74 | ||
|
|
2f9faa53a1 | ||
|
|
718607a758 | ||
|
|
3789156559 | ||
|
|
042ce6f2de |
@@ -1,6 +1,5 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model = self.device.model
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model=self.device.model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer=self.device.manufacturer or "Amazon",
|
||||
hw_version=self.device.hardware_version,
|
||||
sw_version=(
|
||||
self.device.software_version
|
||||
if model != SPEAKER_GROUP_DEVICE_TYPE
|
||||
else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
|
||||
sw_version=self.device.software_version,
|
||||
serial_number=serial_num,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.0.0"]
|
||||
"requirements": ["aioamazondevices==13.0.1"]
|
||||
}
|
||||
|
||||
@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
assert method is not None
|
||||
|
||||
await method(self.device, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self.device.serial_number].sensors[
|
||||
self.entity_description.key
|
||||
].value = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.0"]
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_august(hass, entry, august_gateway)
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
||||
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
|
||||
except (
|
||||
AugustApiAIOHTTPError,
|
||||
OAuth2TokenRequestError,
|
||||
ClientError,
|
||||
CannotConnect,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Diagnostics support for AWS S3."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
DATA_MANAGER as BACKUP_DATA_MANAGER,
|
||||
BackupManager,
|
||||
)
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_PREFIX,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import S3ConfigEntry
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
TO_REDACT = (CONF_ACCESS_KEY_ID, CONF_SECRET_ACCESS_KEY)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: S3ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
|
||||
backups = await async_list_backups_from_s3(
|
||||
coordinator.client,
|
||||
bucket=entry.data[CONF_BUCKET],
|
||||
prefix=entry.data.get(CONF_PREFIX, ""),
|
||||
)
|
||||
|
||||
data = {
|
||||
"coordinator_data": dataclasses.asdict(coordinator.data),
|
||||
"config": {
|
||||
**entry.data,
|
||||
**entry.options,
|
||||
},
|
||||
"backup_agents": [
|
||||
{"name": agent.name}
|
||||
for agent in backup_manager.backup_agents.values()
|
||||
if agent.domain == DOMAIN
|
||||
],
|
||||
"backup": [backup.as_dict() for backup in backups],
|
||||
}
|
||||
|
||||
return async_redact_data(data, TO_REDACT)
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: S3 is a cloud service that is not discovered on the network.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==66"],
|
||||
"requirements": ["axis==67"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -246,6 +246,8 @@ def decrypt_backup(
|
||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error decrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
error = err
|
||||
@@ -332,8 +334,10 @@ def encrypt_backup(
|
||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error encrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
|
||||
error = err
|
||||
else:
|
||||
# Pad the output stream to the requested minimum size
|
||||
|
||||
@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_PASSKEY, DOMAIN
|
||||
from .const import CONF_PASSKEY, DOMAIN, LOGGER
|
||||
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -52,7 +52,7 @@ class BSBLanData:
|
||||
client: BSBLAN
|
||||
device: Device
|
||||
info: Info
|
||||
static: StaticState
|
||||
static: StaticState | None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
# the connection by fetching firmware version
|
||||
await bsblan.initialize()
|
||||
|
||||
# Fetch device metadata in parallel for faster startup
|
||||
device, info, static = await asyncio.gather(
|
||||
# Fetch required device metadata in parallel for faster startup
|
||||
device, info = await asyncio.gather(
|
||||
bsblan.device(),
|
||||
bsblan.info(),
|
||||
bsblan.static_values(),
|
||||
)
|
||||
except BSBLANConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
translation_key="setup_general_error",
|
||||
) from err
|
||||
|
||||
try:
|
||||
static = await bsblan.static_values()
|
||||
except (BSBLANError, TimeoutError) as err:
|
||||
LOGGER.debug(
|
||||
"Static values not available for %s: %s",
|
||||
entry.data[CONF_HOST],
|
||||
err,
|
||||
)
|
||||
static = None
|
||||
|
||||
# Create coordinators with the already-initialized client
|
||||
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
|
||||
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)
|
||||
|
||||
@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
||||
|
||||
# Set temperature range if available, otherwise use Home Assistant defaults
|
||||
if data.static.min_temp is not None and data.static.min_temp.value is not None:
|
||||
self._attr_min_temp = data.static.min_temp.value
|
||||
if data.static.max_temp is not None and data.static.max_temp.value is not None:
|
||||
self._attr_max_temp = data.static.max_temp.value
|
||||
if (static := data.static) is not None:
|
||||
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
|
||||
self._attr_min_temp = min_temp.value
|
||||
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
|
||||
self._attr_max_temp = max_temp.value
|
||||
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||
},
|
||||
"static": data.static.model_dump(),
|
||||
"static": data.static.model_dump() if data.static is not None else None,
|
||||
}
|
||||
|
||||
# Add DHW config and schedule from slow coordinator if available
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.0"],
|
||||
"requirements": ["python-bsblan==5.1.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.9"],
|
||||
"requirements": ["PyChromecast==14.0.10"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.0"]
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
self._use_tls = user_input[CONF_SSL]
|
||||
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
|
||||
|
||||
self._port = self._determine_port(user_input)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from fritzconnection.core.exceptions import (
|
||||
FritzSecurityError,
|
||||
FritzServiceError,
|
||||
)
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
@@ -68,6 +69,7 @@ BUTTON_TYPE_WOL = "WakeOnLan"
|
||||
UPTIME_DEVIATION = 5
|
||||
|
||||
FRITZ_EXCEPTIONS = (
|
||||
ConnectionError,
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
|
||||
@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
|
||||
]
|
||||
)
|
||||
_LOGGER.debug("WiFi networks count: %s", wifi_count)
|
||||
networks: dict = {}
|
||||
networks: dict[int, dict[str, Any]] = {}
|
||||
for i in range(1, wifi_count + 1):
|
||||
network_info = await avm_wrapper.async_get_wlan_configuration(i)
|
||||
# Devices with 4 WLAN services, use the 2nd for internal communications
|
||||
if not (wifi_count == 4 and i == 2):
|
||||
networks[i] = {
|
||||
"ssid": network_info["NewSSID"],
|
||||
"bssid": network_info["NewBSSID"],
|
||||
"standard": network_info["NewStandard"],
|
||||
"enabled": network_info["NewEnable"],
|
||||
"status": network_info["NewStatus"],
|
||||
}
|
||||
networks[i] = network_info
|
||||
for i, network in networks.copy().items():
|
||||
networks[i]["switch_name"] = network["ssid"]
|
||||
networks[i]["switch_name"] = network["NewSSID"]
|
||||
if (
|
||||
len(
|
||||
[
|
||||
j
|
||||
for j, n in networks.items()
|
||||
if slugify(n["ssid"]) == slugify(network["ssid"])
|
||||
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
|
||||
]
|
||||
)
|
||||
> 1
|
||||
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
|
||||
for key, attr in attributes_dict.items():
|
||||
self._attributes[attr] = self.port_mapping[key]
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
|
||||
|
||||
resp = await self._avm_wrapper.async_add_port_mapping(
|
||||
await self._avm_wrapper.async_add_port_mapping(
|
||||
self.connection_type, self.port_mapping
|
||||
)
|
||||
return bool(resp is not None)
|
||||
|
||||
|
||||
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
"""Turn off switch."""
|
||||
await self._async_handle_turn_on_off(turn_on=False)
|
||||
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||
"""Handle switch state change request."""
|
||||
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
|
||||
self._avm_wrapper.devices[self._mac].wan_access = turn_on
|
||||
self.async_write_ha_state()
|
||||
return True
|
||||
|
||||
|
||||
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
avm_wrapper: AvmWrapper,
|
||||
device_friendly_name: str,
|
||||
network_num: int,
|
||||
network_data: dict,
|
||||
network_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Init Fritz Wifi switch."""
|
||||
self._avm_wrapper = avm_wrapper
|
||||
self._wifi_info = network_data
|
||||
|
||||
self._attributes = {}
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
type=SWITCH_TYPE_WIFINETWORK,
|
||||
callback_update=self._async_fetch_update,
|
||||
callback_switch=self._async_switch_on_off_executor,
|
||||
init_state=network_data["enabled"],
|
||||
init_state=network_data["NewEnable"],
|
||||
)
|
||||
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
|
||||
|
||||
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
self._attributes["mac_address_control"] = wifi_info[
|
||||
"NewMACAddressControlEnabled"
|
||||
]
|
||||
self._wifi_info = wifi_info
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
"""Handle wifi switch."""
|
||||
self._wifi_info["NewEnable"] = turn_on
|
||||
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260304.0"]
|
||||
"requirements": ["home-assistant-frontend==20260312.1"]
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class GhostConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
site_title = site["title"]
|
||||
|
||||
await self.async_set_unique_id(site["uuid"])
|
||||
await self.async_set_unique_id(site["site_uuid"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_weather_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-google-weather-api==0.0.4"]
|
||||
"requirements": ["python-google-weather-api==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==2.3.0"]
|
||||
"requirements": ["govee-local-api==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["greenplanet-energy-api==0.1.4"],
|
||||
"requirements": ["greenplanet-energy-api==0.1.10"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -117,13 +117,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
# Map raw event type names to friendly names using SENSOR_MAP
|
||||
mapped_events: dict[str, list[int]] = {}
|
||||
for event_type, channels in nvr_events.items():
|
||||
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
|
||||
event_key = event_type.lower()
|
||||
# Skip videoloss - used as watchdog by pyhik, not a real sensor
|
||||
if event_key == "videoloss":
|
||||
continue
|
||||
friendly_name = SENSOR_MAP.get(event_key)
|
||||
if friendly_name is None:
|
||||
_LOGGER.debug("Skipping unmapped event type: %s", event_type)
|
||||
continue
|
||||
if friendly_name in mapped_events:
|
||||
mapped_events[friendly_name].extend(channels)
|
||||
else:
|
||||
mapped_events[friendly_name] = list(channels)
|
||||
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
|
||||
camera.inject_events(mapped_events)
|
||||
if mapped_events:
|
||||
camera.inject_events(mapped_events)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"No event triggers returned from %s. "
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from apyhiveapi import Auth
|
||||
@@ -26,6 +27,8 @@ from homeassistant.core import callback
|
||||
from . import HiveConfigEntry
|
||||
from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Hive config flow."""
|
||||
@@ -36,7 +39,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
self.tokens: dict[str, str] = {}
|
||||
self.tokens: dict[str, Any] = {}
|
||||
self.device_registration: bool = False
|
||||
self.device_name = "Home Assistant"
|
||||
|
||||
@@ -67,11 +70,22 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except HiveApiError:
|
||||
errors["base"] = "no_internet_available"
|
||||
|
||||
if (
|
||||
auth_result := self.tokens.get("AuthenticationResult", {})
|
||||
) and auth_result.get("NewDeviceMetadata"):
|
||||
_LOGGER.debug("Login successful, New device detected")
|
||||
self.device_registration = True
|
||||
return await self.async_step_configuration()
|
||||
|
||||
if self.tokens.get("ChallengeName") == "SMS_MFA":
|
||||
_LOGGER.debug("Login successful, SMS 2FA required")
|
||||
# Complete SMS 2FA.
|
||||
return await self.async_step_2fa()
|
||||
|
||||
if not errors:
|
||||
_LOGGER.debug(
|
||||
"Login successful, no new device detected, no 2FA required"
|
||||
)
|
||||
# Complete the entry.
|
||||
try:
|
||||
return await self.async_setup_hive_entry()
|
||||
@@ -103,6 +117,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "no_internet_available"
|
||||
|
||||
if not errors:
|
||||
_LOGGER.debug("2FA successful")
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return await self.async_setup_hive_entry()
|
||||
self.device_registration = True
|
||||
@@ -119,10 +134,11 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input:
|
||||
if self.device_registration:
|
||||
_LOGGER.debug("Attempting to register device")
|
||||
self.device_name = user_input["device_name"]
|
||||
await self.hive_auth.device_registration(user_input["device_name"])
|
||||
self.data["device_data"] = await self.hive_auth.get_device_data()
|
||||
|
||||
_LOGGER.debug("Device registration successful")
|
||||
try:
|
||||
return await self.async_setup_hive_entry()
|
||||
except UnknownHiveError:
|
||||
@@ -142,6 +158,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
raise UnknownHiveError
|
||||
|
||||
# Setup the config entry
|
||||
_LOGGER.debug("Setting up Hive entry")
|
||||
self.data["tokens"] = self.tokens
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
@@ -160,6 +177,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||
}
|
||||
_LOGGER.debug("Reauthenticating user")
|
||||
return await self.async_step_user(data)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -91,14 +91,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="energy_exported",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_imported",
|
||||
translation_key="energy_imported",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="frequency",
|
||||
|
||||
@@ -610,6 +610,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
||||
key="active_liter_lpm",
|
||||
translation_key="active_liter_lpm",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
has_fn=lambda data: data.measurement.active_liter_lpm is not None,
|
||||
value_fn=lambda data: data.measurement.active_liter_lpm,
|
||||
|
||||
@@ -901,7 +901,9 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
|
||||
)
|
||||
|
||||
|
||||
class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined):
|
||||
class PowerViewShadeDualOverlappedCombinedTilt(
|
||||
PowerViewShadeDualOverlappedCombined, PowerViewShadeWithTiltBase
|
||||
):
|
||||
"""Represent a shade that has a front sheer and rear opaque panel.
|
||||
|
||||
This equates to two shades being controlled by one motor.
|
||||
@@ -915,26 +917,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
||||
Type 10 - Duolite with 180° Tilt
|
||||
"""
|
||||
|
||||
# type
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PowerviewShadeUpdateCoordinator,
|
||||
device_info: PowerviewDeviceInfo,
|
||||
room_name: str,
|
||||
shade: BaseShade,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize the shade."""
|
||||
super().__init__(coordinator, device_info, room_name, shade, name)
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION
|
||||
)
|
||||
if self._shade.is_supported(MOTION_STOP):
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||
self._max_tilt = self._shade.shade_limits.tilt_max
|
||||
|
||||
@property
|
||||
def transition_steps(self) -> int:
|
||||
"""Return the steps to make a move."""
|
||||
@@ -949,26 +931,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
||||
tilt = self.positions.tilt
|
||||
return ceil(primary + secondary + tilt)
|
||||
|
||||
@callback
|
||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
|
||||
"""Return a ShadePosition."""
|
||||
return ShadePosition(
|
||||
tilt=target_hass_tilt_position,
|
||||
velocity=self.positions.velocity,
|
||||
)
|
||||
|
||||
@property
|
||||
def open_tilt_position(self) -> ShadePosition:
|
||||
"""Return the open tilt position and required additional positions."""
|
||||
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
|
||||
|
||||
@property
|
||||
def close_tilt_position(self) -> ShadePosition:
|
||||
"""Return the open tilt position and required additional positions."""
|
||||
return replace(
|
||||
self._shade.close_position_tilt, velocity=self.positions.velocity
|
||||
)
|
||||
|
||||
|
||||
TYPE_TO_CLASSES = {
|
||||
0: (PowerViewShade,),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hyponcloud==0.3.0"]
|
||||
"requirements": ["hyponcloud==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -21,11 +21,17 @@ from .coordinator import HypontechConfigEntry, HypontechDataCoordinator
|
||||
from .entity import HypontechEntity, HypontechPlantEntity
|
||||
|
||||
|
||||
def _power_unit(data: OverviewData | PlantData) -> str:
|
||||
"""Return the unit of measurement for power based on the API unit."""
|
||||
return UnitOfPower.KILO_WATT if data.company.upper() == "KW" else UnitOfPower.WATT
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HypontechSensorDescription(SensorEntityDescription):
|
||||
"""Describes Hypontech overview sensor entity."""
|
||||
|
||||
value_fn: Callable[[OverviewData], float | None]
|
||||
unit_fn: Callable[[OverviewData], str] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -33,15 +39,16 @@ class HypontechPlantSensorDescription(SensorEntityDescription):
|
||||
"""Describes Hypontech plant sensor entity."""
|
||||
|
||||
value_fn: Callable[[PlantData], float | None]
|
||||
unit_fn: Callable[[PlantData], str] | None = None
|
||||
|
||||
|
||||
OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
|
||||
HypontechSensorDescription(
|
||||
key="pv_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda c: c.power,
|
||||
unit_fn=_power_unit,
|
||||
),
|
||||
HypontechSensorDescription(
|
||||
key="lifetime_energy",
|
||||
@@ -64,10 +71,10 @@ OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
|
||||
PLANT_SENSORS: tuple[HypontechPlantSensorDescription, ...] = (
|
||||
HypontechPlantSensorDescription(
|
||||
key="pv_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda c: c.power,
|
||||
unit_fn=_power_unit,
|
||||
),
|
||||
HypontechPlantSensorDescription(
|
||||
key="lifetime_energy",
|
||||
@@ -124,6 +131,13 @@ class HypontechOverviewSensor(HypontechEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.account_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_fn is not None:
|
||||
return self.entity_description.unit_fn(self.coordinator.data.overview)
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
@@ -146,6 +160,13 @@ class HypontechPlantSensor(HypontechPlantEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{plant_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_fn is not None:
|
||||
return self.entity_description.unit_fn(self.plant)
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
IntellifireSensorEntityDescription(
|
||||
key="timer_end_timestamp",
|
||||
translation_key="timer_end_timestamp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=_time_remaining_to_timestamp,
|
||||
),
|
||||
|
||||
@@ -221,13 +221,13 @@ class IntesisAC(ClimateEntity):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device specific state attributes."""
|
||||
attrs = {}
|
||||
if self._outdoor_temp:
|
||||
if self._outdoor_temp is not None:
|
||||
attrs["outdoor_temp"] = self._outdoor_temp
|
||||
if self._power_consumption_heat:
|
||||
if self._power_consumption_heat is not None:
|
||||
attrs["power_consumption_heat_kw"] = round(
|
||||
self._power_consumption_heat / 1000, 1
|
||||
)
|
||||
if self._power_consumption_cool:
|
||||
if self._power_consumption_cool is not None:
|
||||
attrs["power_consumption_cool_kw"] = round(
|
||||
self._power_consumption_cool / 1000, 1
|
||||
)
|
||||
@@ -244,7 +244,7 @@ class IntesisAC(ClimateEntity):
|
||||
if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
if temperature := kwargs.get(ATTR_TEMPERATURE):
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
_LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
|
||||
await self._controller.set_temperature(self._device_id, temperature)
|
||||
self._attr_target_temperature = temperature
|
||||
@@ -271,7 +271,7 @@ class IntesisAC(ClimateEntity):
|
||||
await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
|
||||
|
||||
# Send the temperature again in case changing modes has changed it
|
||||
if self._attr_target_temperature:
|
||||
if self._attr_target_temperature is not None:
|
||||
await self._controller.set_temperature(
|
||||
self._device_id, self._attr_target_temperature
|
||||
)
|
||||
|
||||
@@ -358,7 +358,7 @@ PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
|
||||
native_max_value=MAX_TEMP,
|
||||
native_min_value_f=MIN_TEMP_F,
|
||||
native_max_value_f=MAX_TEMP_F,
|
||||
native_step=5,
|
||||
native_step=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==2.0.1"]
|
||||
"requirements": ["pyjvcprojector==2.0.3"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aionotify", "evdev"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evdev==1.6.1", "asyncinotify==4.2.0"]
|
||||
"requirements": ["evdev==1.9.3", "asyncinotify==4.4.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
|
||||
from xknx.dpt.dpt_16 import DPTString
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import UnitOfReactiveEnergy
|
||||
|
||||
HaDptClass = Literal["numeric", "enum", "complex", "string"]
|
||||
|
||||
@@ -36,7 +37,7 @@ def get_supported_dpts() -> Mapping[str, DPTInfo]:
|
||||
main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
|
||||
sub=dpt_class.dpt_sub_number,
|
||||
name=dpt_class.value_type,
|
||||
unit=dpt_class.unit,
|
||||
unit=_sensor_unit_overrides.get(dpt_number_str, dpt_class.unit),
|
||||
sensor_device_class=_sensor_device_classes.get(dpt_number_str),
|
||||
sensor_state_class=_get_sensor_state_class(ha_dpt_class, dpt_number_str),
|
||||
)
|
||||
@@ -77,13 +78,13 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"12.1200": SensorDeviceClass.VOLUME,
|
||||
"12.1201": SensorDeviceClass.VOLUME,
|
||||
"13.002": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"13.010": SensorDeviceClass.ENERGY,
|
||||
"13.012": SensorDeviceClass.REACTIVE_ENERGY,
|
||||
"13.013": SensorDeviceClass.ENERGY,
|
||||
"13.015": SensorDeviceClass.REACTIVE_ENERGY,
|
||||
"13.016": SensorDeviceClass.ENERGY,
|
||||
"13.1200": SensorDeviceClass.VOLUME,
|
||||
"13.1201": SensorDeviceClass.VOLUME,
|
||||
"13.010": SensorDeviceClass.ENERGY, # DPTActiveEnergy
|
||||
"13.012": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergy
|
||||
"13.013": SensorDeviceClass.ENERGY, # DPTActiveEnergykWh
|
||||
"13.015": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergykVARh
|
||||
"13.016": SensorDeviceClass.ENERGY, # DPTActiveEnergyMWh
|
||||
"13.1200": SensorDeviceClass.VOLUME, # DPTDeltaVolumeLiquidLitre
|
||||
"13.1201": SensorDeviceClass.VOLUME, # DPTDeltaVolumeM3
|
||||
"14.010": SensorDeviceClass.AREA,
|
||||
"14.019": SensorDeviceClass.CURRENT,
|
||||
"14.027": SensorDeviceClass.VOLTAGE,
|
||||
@@ -91,7 +92,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"14.030": SensorDeviceClass.VOLTAGE,
|
||||
"14.031": SensorDeviceClass.ENERGY,
|
||||
"14.033": SensorDeviceClass.FREQUENCY,
|
||||
"14.037": SensorDeviceClass.ENERGY_STORAGE,
|
||||
"14.037": SensorDeviceClass.ENERGY_STORAGE, # DPTHeatQuantity
|
||||
"14.039": SensorDeviceClass.DISTANCE,
|
||||
"14.051": SensorDeviceClass.WEIGHT,
|
||||
"14.056": SensorDeviceClass.POWER,
|
||||
@@ -101,7 +102,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"14.068": SensorDeviceClass.TEMPERATURE,
|
||||
"14.069": SensorDeviceClass.TEMPERATURE,
|
||||
"14.070": SensorDeviceClass.TEMPERATURE_DELTA,
|
||||
"14.076": SensorDeviceClass.VOLUME,
|
||||
"14.076": SensorDeviceClass.VOLUME, # DPTVolume
|
||||
"14.077": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"14.080": SensorDeviceClass.APPARENT_POWER,
|
||||
"14.1200": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
@@ -121,17 +122,28 @@ _sensor_state_class_overrides: Mapping[str, SensorStateClass | None] = {
|
||||
"13.010": SensorStateClass.TOTAL, # DPTActiveEnergy
|
||||
"13.011": SensorStateClass.TOTAL, # DPTApparantEnergy
|
||||
"13.012": SensorStateClass.TOTAL, # DPTReactiveEnergy
|
||||
"13.013": SensorStateClass.TOTAL, # DPTActiveEnergykWh
|
||||
"13.015": SensorStateClass.TOTAL, # DPTReactiveEnergykVARh
|
||||
"13.016": SensorStateClass.TOTAL, # DPTActiveEnergyMWh
|
||||
"13.1200": SensorStateClass.TOTAL, # DPTDeltaVolumeLiquidLitre
|
||||
"13.1201": SensorStateClass.TOTAL, # DPTDeltaVolumeM3
|
||||
"14.007": SensorStateClass.MEASUREMENT_ANGLE, # DPTAngleDeg
|
||||
"14.037": SensorStateClass.TOTAL, # DPTHeatQuantity
|
||||
"14.051": SensorStateClass.TOTAL, # DPTMass
|
||||
"14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg
|
||||
"14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy
|
||||
"14.076": SensorStateClass.TOTAL, # DPTVolume
|
||||
"17.001": None, # DPTSceneNumber
|
||||
"29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte
|
||||
"29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte
|
||||
"29.012": SensorStateClass.TOTAL, # DPTReactiveEnergy8Byte
|
||||
}
|
||||
|
||||
_sensor_unit_overrides: Mapping[str, str] = {
|
||||
"13.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy (VARh in KNX)
|
||||
"13.015": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergykVARh (kVARh in KNX)
|
||||
"29.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy8Byte (VARh in KNX)
|
||||
}
|
||||
|
||||
|
||||
def _get_sensor_state_class(
|
||||
ha_dpt_class: HaDptClass, dpt_number_str: str
|
||||
|
||||
@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
"""Representation of an LG soundbar device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_state = MediaPlayerState.ON
|
||||
_attr_state = MediaPlayerState.OFF
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -79,6 +79,8 @@ class LGDevice(MediaPlayerEntity):
|
||||
self._treble = 0
|
||||
self._device = None
|
||||
self._support_play_control = False
|
||||
self._device_on = False
|
||||
self._stream_type = 0
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)}, name=host
|
||||
)
|
||||
@@ -113,6 +115,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
if "i_curr_func" in data:
|
||||
self._function = data["i_curr_func"]
|
||||
if "b_powerstatus" in data:
|
||||
self._device_on = data["b_powerstatus"]
|
||||
if data["b_powerstatus"]:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
@@ -157,17 +160,34 @@ class LGDevice(MediaPlayerEntity):
|
||||
|
||||
def _update_playinfo(self, data: dict[str, Any]) -> None:
|
||||
"""Update the player info."""
|
||||
if "i_stream_type" in data:
|
||||
if self._stream_type != data["i_stream_type"]:
|
||||
self._stream_type = data["i_stream_type"]
|
||||
# Ask device for current play info when stream type changed.
|
||||
self._device.get_play()
|
||||
if data["i_stream_type"] == 0:
|
||||
# If the stream type is 0 (aka the soundbar is used as an actual soundbar)
|
||||
# the last track info should be cleared and the state should only be on or off,
|
||||
# as all playing/paused are not applicable in this mode
|
||||
self._attr_media_image_url = None
|
||||
self._attr_media_artist = None
|
||||
self._attr_media_title = None
|
||||
if self._device_on:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
if "i_play_ctrl" in data:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if self._device_on and self._stream_type != 0:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if "s_albumart" in data:
|
||||
self._attr_media_image_url = data["s_albumart"]
|
||||
self._attr_media_image_url = data["s_albumart"].strip() or None
|
||||
if "s_artist" in data:
|
||||
self._attr_media_artist = data["s_artist"]
|
||||
self._attr_media_artist = data["s_artist"].strip() or None
|
||||
if "s_title" in data:
|
||||
self._attr_media_title = data["s_title"]
|
||||
self._attr_media_title = data["s_title"].strip() or None
|
||||
if "b_support_play_ctrl" in data:
|
||||
self._support_play_control = data["b_support_play_ctrl"]
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
# Matter server.
|
||||
_attr_should_poll = True
|
||||
_software_update: MatterSoftwareVersion | None = None
|
||||
_installed_software_version: int | None = None
|
||||
_cancel_update: CALLBACK_TYPE | None = None
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL
|
||||
@@ -92,6 +93,9 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
|
||||
self._installed_software_version = self.get_matter_attribute_value(
|
||||
clusters.BasicInformation.Attributes.SoftwareVersion
|
||||
)
|
||||
self._attr_installed_version = self.get_matter_attribute_value(
|
||||
clusters.BasicInformation.Attributes.SoftwareVersionString
|
||||
)
|
||||
@@ -123,6 +127,22 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
else:
|
||||
self._attr_update_percentage = None
|
||||
|
||||
def _format_latest_version(
|
||||
self, update_information: MatterSoftwareVersion
|
||||
) -> str | None:
|
||||
"""Return the version string to expose in Home Assistant."""
|
||||
latest_version = update_information.software_version_string
|
||||
if self._installed_software_version is None:
|
||||
return latest_version
|
||||
|
||||
if update_information.software_version == self._installed_software_version:
|
||||
return self._attr_installed_version or latest_version
|
||||
|
||||
if latest_version == self._attr_installed_version:
|
||||
return f"{latest_version} ({update_information.software_version})"
|
||||
|
||||
return latest_version
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Call when the entity needs to be updated."""
|
||||
try:
|
||||
@@ -130,11 +150,13 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
node_id=self._endpoint.node.node_id
|
||||
)
|
||||
if not update_information:
|
||||
self._software_update = None
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
self._attr_release_url = None
|
||||
return
|
||||
|
||||
self._software_update = update_information
|
||||
self._attr_latest_version = update_information.software_version_string
|
||||
self._attr_latest_version = self._format_latest_version(update_information)
|
||||
self._attr_release_url = update_information.release_notes_url
|
||||
|
||||
except UpdateCheckError as err:
|
||||
@@ -212,7 +234,12 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
|
||||
software_version: str | int | None = version
|
||||
if self._software_update is not None and (
|
||||
version is None or version == self._software_update.software_version_string
|
||||
version is None
|
||||
or version
|
||||
in {
|
||||
self._software_update.software_version_string,
|
||||
self._attr_latest_version,
|
||||
}
|
||||
):
|
||||
# Update to the version previously fetched and shown.
|
||||
# We can pass the integer version directly to speedup download.
|
||||
|
||||
@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
|
||||
finished = 522, 11012
|
||||
extra_dry = 523
|
||||
hand_iron = 524
|
||||
hygiene_drying = 525
|
||||
moisten = 526
|
||||
thermo_spin = 527
|
||||
timed_drying = 528
|
||||
@@ -615,13 +616,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
pyrolytic = 323
|
||||
descale = 326
|
||||
evaporate_water = 327
|
||||
rinse = 333
|
||||
shabbat_program = 335
|
||||
yom_tov = 336
|
||||
drying = 357
|
||||
hydroclean = 341
|
||||
drying = 357, 2028
|
||||
heat_crockery = 358
|
||||
prove_dough = 359
|
||||
prove_dough = 359, 2023
|
||||
low_temperature_cooking = 360
|
||||
steam_cooking = 361
|
||||
steam_cooking = 8, 361
|
||||
keeping_warm = 362
|
||||
apple_sponge = 364
|
||||
apple_pie = 365
|
||||
@@ -668,9 +671,9 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
saddle_of_roebuck = 456
|
||||
salmon_fillet = 461
|
||||
potato_cheese_gratin = 464
|
||||
trout = 486
|
||||
carp = 491
|
||||
salmon_trout = 492
|
||||
trout = 486, 2224
|
||||
carp = 491, 2233
|
||||
salmon_trout = 492, 2241
|
||||
springform_tin_15cm = 496
|
||||
springform_tin_20cm = 497
|
||||
springform_tin_25cm = 498
|
||||
@@ -721,7 +724,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
belgian_sponge_cake = 624
|
||||
goose_unstuffed = 625
|
||||
rack_of_lamb_with_vegetables = 634
|
||||
yorkshire_pudding = 635
|
||||
yorkshire_pudding = 635, 2352
|
||||
meat_loaf = 636
|
||||
defrost_meat = 647
|
||||
defrost_vegetables = 654
|
||||
@@ -736,137 +739,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
pork_belly = 701
|
||||
pikeperch_fillet_with_vegetables = 702
|
||||
steam_bake = 99001
|
||||
|
||||
|
||||
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for dish warmers."""
|
||||
|
||||
no_program = 0, -1
|
||||
warm_cups_glasses = 1
|
||||
warm_dishes_plates = 2
|
||||
keep_warm = 3
|
||||
slow_roasting = 4
|
||||
|
||||
|
||||
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for robot vacuum cleaners."""
|
||||
|
||||
no_program = 0, -1
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for coffee systems."""
|
||||
|
||||
no_program = 0, -1
|
||||
|
||||
check_appliance = 17004
|
||||
|
||||
# profile 1
|
||||
ristretto = 24000, 24032, 24064, 24096, 24128
|
||||
espresso = 24001, 24033, 24065, 24097, 24129
|
||||
coffee = 24002, 24034, 24066, 24098, 24130
|
||||
long_coffee = 24003, 24035, 24067, 24099, 24131
|
||||
cappuccino = 24004, 24036, 24068, 24100, 24132
|
||||
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
|
||||
latte_macchiato = 24006, 24038, 24070, 24102, 24134
|
||||
espresso_macchiato = 24007, 24039, 24071, 24135
|
||||
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
|
||||
caffe_latte = 24009, 24041, 24073, 24105, 24137
|
||||
flat_white = 24012, 24044, 24076, 24108, 24140
|
||||
very_hot_water = 24013, 24045, 24077, 24109, 24141
|
||||
hot_water = 24014, 24046, 24078, 24110, 24142
|
||||
hot_milk = 24015, 24047, 24079, 24111, 24143
|
||||
milk_foam = 24016, 24048, 24080, 24112, 24144
|
||||
black_tea = 24017, 24049, 24081, 24113, 24145
|
||||
herbal_tea = 24018, 24050, 24082, 24114, 24146
|
||||
fruit_tea = 24019, 24051, 24083, 24115, 24147
|
||||
green_tea = 24020, 24052, 24084, 24116, 24148
|
||||
white_tea = 24021, 24053, 24085, 24117, 24149
|
||||
japanese_tea = 24022, 29054, 24086, 24118, 24150
|
||||
# special programs
|
||||
coffee_pot = 24400
|
||||
barista_assistant = 24407
|
||||
# machine settings menu
|
||||
appliance_settings = (
|
||||
16016, # display brightness
|
||||
16018, # volume
|
||||
16019, # buttons volume
|
||||
16020, # child lock
|
||||
16021, # water hardness
|
||||
16027, # welcome sound
|
||||
16033, # connection status
|
||||
16035, # remote control
|
||||
16037, # remote update
|
||||
24500, # total dispensed
|
||||
24502, # lights appliance on
|
||||
24503, # lights appliance off
|
||||
24504, # turn off lights after
|
||||
24506, # altitude
|
||||
24513, # performance mode
|
||||
24516, # turn off after
|
||||
24537, # advanced mode
|
||||
24542, # tea timer
|
||||
24549, # total coffee dispensed
|
||||
24550, # total tea dispensed
|
||||
24551, # total ristretto
|
||||
24552, # total cappuccino
|
||||
24553, # total espresso
|
||||
24554, # total coffee
|
||||
24555, # total long coffee
|
||||
24556, # total italian cappuccino
|
||||
24557, # total latte macchiato
|
||||
24558, # total caffe latte
|
||||
24560, # total espresso macchiato
|
||||
24562, # total flat white
|
||||
24563, # total coffee with milk
|
||||
24564, # total black tea
|
||||
24565, # total herbal tea
|
||||
24566, # total fruit tea
|
||||
24567, # total green tea
|
||||
24568, # total white tea
|
||||
24569, # total japanese tea
|
||||
24571, # total milk foam
|
||||
24572, # total hot milk
|
||||
24573, # total hot water
|
||||
24574, # total very hot water
|
||||
24575, # counter to descaling
|
||||
24576, # counter to brewing unit degreasing
|
||||
24800, # maintenance
|
||||
24801, # profiles settings menu
|
||||
24813, # add profile
|
||||
)
|
||||
appliance_rinse = 24750, 24759, 24773, 24787, 24788
|
||||
intermediate_rinsing = 24758
|
||||
automatic_maintenance = 24778
|
||||
descaling = 24751
|
||||
brewing_unit_degrease = 24753
|
||||
milk_pipework_rinse = 24754
|
||||
milk_pipework_clean = 24789
|
||||
|
||||
|
||||
class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for steam oven micro combo."""
|
||||
|
||||
no_program = 0, -1
|
||||
steam_cooking = 8
|
||||
microwave = 19
|
||||
popcorn = 53
|
||||
quick_mw = 54
|
||||
sous_vide = 72
|
||||
eco_steam_cooking = 75
|
||||
rapid_steam_cooking = 77
|
||||
descale = 326
|
||||
menu_cooking = 330
|
||||
reheating_with_steam = 2018
|
||||
defrosting_with_steam = 2019
|
||||
blanching = 2020
|
||||
bottling = 2021
|
||||
sterilize_crockery = 2022
|
||||
prove_dough = 2023
|
||||
soak = 2027
|
||||
reheating_with_microwave = 2029
|
||||
defrosting_with_microwave = 2030
|
||||
@@ -1020,18 +901,15 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
gilt_head_bream_fillet = 2220
|
||||
codfish_piece = 2221, 2232
|
||||
codfish_fillet = 2222, 2231
|
||||
trout = 2224
|
||||
pike_fillet = 2225
|
||||
pike_piece = 2226
|
||||
halibut_fillet_2_cm = 2227
|
||||
halibut_fillet_3_cm = 2230
|
||||
carp = 2233
|
||||
salmon_fillet_2_cm = 2234
|
||||
salmon_fillet_3_cm = 2235
|
||||
salmon_steak_2_cm = 2238
|
||||
salmon_steak_3_cm = 2239
|
||||
salmon_piece = 2240
|
||||
salmon_trout = 2241
|
||||
iridescent_shark_fillet = 2244
|
||||
red_snapper_fillet_2_cm = 2245
|
||||
red_snapper_fillet_3_cm = 2248
|
||||
@@ -1246,7 +1124,7 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
wholegrain_rice = 3376
|
||||
parboiled_rice_steam_cooking = 3380
|
||||
parboiled_rice_rapid_steam_cooking = 3381
|
||||
basmati_rice_steam_cooking = 3383
|
||||
basmati_rice_steam_cooking = 3382, 3383
|
||||
basmati_rice_rapid_steam_cooking = 3384
|
||||
jasmine_rice_steam_cooking = 3386
|
||||
jasmine_rice_rapid_steam_cooking = 3387
|
||||
@@ -1254,7 +1132,7 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
huanghuanian_rapid_steam_cooking = 3390
|
||||
simiao_steam_cooking = 3392
|
||||
simiao_rapid_steam_cooking = 3393
|
||||
long_grain_rice_general_steam_cooking = 3395
|
||||
long_grain_rice_general_steam_cooking = 3394, 3395
|
||||
long_grain_rice_general_rapid_steam_cooking = 3396
|
||||
chongming_steam_cooking = 3398
|
||||
chongming_rapid_steam_cooking = 3399
|
||||
@@ -1268,6 +1146,116 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
round_grain_rice_general_rapid_steam_cooking = 3411
|
||||
|
||||
|
||||
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for dish warmers."""
|
||||
|
||||
no_program = 0, -1
|
||||
warm_cups_glasses = 1
|
||||
warm_dishes_plates = 2
|
||||
keep_warm = 3
|
||||
slow_roasting = 4
|
||||
|
||||
|
||||
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for robot vacuum cleaners."""
|
||||
|
||||
no_program = 0, -1
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for coffee systems."""
|
||||
|
||||
no_program = 0, -1
|
||||
|
||||
check_appliance = 17004
|
||||
|
||||
# profile 1
|
||||
ristretto = 24000, 24032, 24064, 24096, 24128
|
||||
espresso = 24001, 24033, 24065, 24097, 24129
|
||||
coffee = 24002, 24034, 24066, 24098, 24130
|
||||
long_coffee = 24003, 24035, 24067, 24099, 24131
|
||||
cappuccino = 24004, 24036, 24068, 24100, 24132
|
||||
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
|
||||
latte_macchiato = 24006, 24038, 24070, 24102, 24134
|
||||
espresso_macchiato = 24007, 24039, 24071, 24135
|
||||
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
|
||||
caffe_latte = 24009, 24041, 24073, 24105, 24137
|
||||
flat_white = 24012, 24044, 24076, 24108, 24140
|
||||
very_hot_water = 24013, 24045, 24077, 24109, 24141
|
||||
hot_water = 24014, 24046, 24078, 24110, 24142
|
||||
hot_milk = 24015, 24047, 24079, 24111, 24143
|
||||
milk_foam = 24016, 24048, 24080, 24112, 24144
|
||||
black_tea = 24017, 24049, 24081, 24113, 24145
|
||||
herbal_tea = 24018, 24050, 24082, 24114, 24146
|
||||
fruit_tea = 24019, 24051, 24083, 24115, 24147
|
||||
green_tea = 24020, 24052, 24084, 24116, 24148
|
||||
white_tea = 24021, 24053, 24085, 24117, 24149
|
||||
japanese_tea = 24022, 29054, 24086, 24118, 24150
|
||||
# special programs
|
||||
coffee_pot = 24400
|
||||
barista_assistant = 24407
|
||||
# machine settings menu
|
||||
appliance_settings = (
|
||||
16016, # display brightness
|
||||
16018, # volume
|
||||
16019, # buttons volume
|
||||
16020, # child lock
|
||||
16021, # water hardness
|
||||
16027, # welcome sound
|
||||
16033, # connection status
|
||||
16035, # remote control
|
||||
16037, # remote update
|
||||
24500, # total dispensed
|
||||
24502, # lights appliance on
|
||||
24503, # lights appliance off
|
||||
24504, # turn off lights after
|
||||
24506, # altitude
|
||||
24513, # performance mode
|
||||
24516, # turn off after
|
||||
24537, # advanced mode
|
||||
24542, # tea timer
|
||||
24549, # total coffee dispensed
|
||||
24550, # total tea dispensed
|
||||
24551, # total ristretto
|
||||
24552, # total cappuccino
|
||||
24553, # total espresso
|
||||
24554, # total coffee
|
||||
24555, # total long coffee
|
||||
24556, # total italian cappuccino
|
||||
24557, # total latte macchiato
|
||||
24558, # total caffe latte
|
||||
24560, # total espresso macchiato
|
||||
24562, # total flat white
|
||||
24563, # total coffee with milk
|
||||
24564, # total black tea
|
||||
24565, # total herbal tea
|
||||
24566, # total fruit tea
|
||||
24567, # total green tea
|
||||
24568, # total white tea
|
||||
24569, # total japanese tea
|
||||
24571, # total milk foam
|
||||
24572, # total hot milk
|
||||
24573, # total hot water
|
||||
24574, # total very hot water
|
||||
24575, # counter to descaling
|
||||
24576, # counter to brewing unit degreasing
|
||||
24800, # maintenance
|
||||
24801, # profiles settings menu
|
||||
24813, # add profile
|
||||
)
|
||||
appliance_rinse = 24750, 24759, 24773, 24787, 24788
|
||||
intermediate_rinsing = 24758
|
||||
automatic_maintenance = 24778
|
||||
descaling = 24751
|
||||
brewing_unit_degrease = 24753
|
||||
milk_pipework_rinse = 24754
|
||||
milk_pipework_clean = 24789
|
||||
|
||||
|
||||
PROGRAM_IDS: dict[int, type[MieleEnum]] = {
|
||||
MieleAppliance.WASHING_MACHINE: WashingMachineProgramId,
|
||||
MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId,
|
||||
@@ -1278,7 +1266,7 @@ PROGRAM_IDS: dict[int, type[MieleEnum]] = {
|
||||
MieleAppliance.STEAM_OVEN_MK2: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN_MICRO: SteamOvenMicroProgramId,
|
||||
MieleAppliance.STEAM_OVEN_MICRO: OvenProgramId,
|
||||
MieleAppliance.WASHER_DRYER: WashingMachineProgramId,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId,
|
||||
MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId,
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"drain_spin": "Drain/spin",
|
||||
"drop_cookies_1_tray": "Drop cookies (1 tray)",
|
||||
"drop_cookies_2_trays": "Drop cookies (2 trays)",
|
||||
"drying": "Drying",
|
||||
"duck": "Duck",
|
||||
"dutch_hash": "Dutch hash",
|
||||
"easy_care": "Easy care",
|
||||
@@ -559,6 +560,7 @@
|
||||
"hot_water": "Hot water",
|
||||
"huanghuanian_rapid_steam_cooking": "Huanghuanian (rapid steam cooking)",
|
||||
"huanghuanian_steam_cooking": "Huanghuanian (steam cooking)",
|
||||
"hydroclean": "HydroClean",
|
||||
"hygiene": "Hygiene",
|
||||
"intensive": "Intensive",
|
||||
"intensive_bake": "Intensive bake",
|
||||
@@ -1005,6 +1007,7 @@
|
||||
"heating_up_phase": "Heating up phase",
|
||||
"hot_milk": "Hot milk",
|
||||
"hygiene": "Hygiene",
|
||||
"hygiene_drying": "Hygiene drying",
|
||||
"interim_rinse": "Interim rinse",
|
||||
"keep_warm": "Keep warm",
|
||||
"keeping_warm": "Keeping warm",
|
||||
|
||||
@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||
latitude: float | None
|
||||
longitude: float | None
|
||||
gps_accuracy: float
|
||||
# Reset manually set location to allow automatic zone detection
|
||||
self._attr_location_name = None
|
||||
if isinstance(
|
||||
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
|
||||
) and isinstance(
|
||||
|
||||
@@ -24,7 +24,7 @@ SUBENTRY_TYPE_ZONE = "zone"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_PORT = 4999
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
DEFAULT_INFER_ARMING_STATE = False
|
||||
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION
|
||||
|
||||
|
||||
@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"device_info": client.device_info,
|
||||
"vehicles": client.vehicles,
|
||||
"ct_connected": client.ct_connected,
|
||||
"cap_available": client.cap_available,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["ohme==1.6.0"]
|
||||
"requirements": ["ohme==1.7.0"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.4"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.4"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.17.0"]
|
||||
"requirements": ["opower==0.17.1"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["oralb_ble"],
|
||||
"requirements": ["oralb-ble==1.0.2"]
|
||||
"requirements": ["oralb-ble==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0"]
|
||||
"requirements": ["python-otbr-api==2.9.0"]
|
||||
}
|
||||
|
||||
@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
|
||||
self._abort_if_unique_id_configured()
|
||||
# Logic that can be reverted back once the new unique ID is in
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_API_TOKEN]
|
||||
)
|
||||
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_update_reload_and_abort(
|
||||
reconf_entry,
|
||||
data_updates={
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.31"]
|
||||
"requirements": ["pyportainer==1.0.33"]
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
ProxmoxContainerButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -277,7 +277,8 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the node button action via executor."""
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes"):
|
||||
node_id = self._node_data.node["node"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_node_power",
|
||||
@@ -285,7 +286,7 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
self._node_data.node["node"],
|
||||
node_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -309,7 +310,8 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the VM button action via executor."""
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms"):
|
||||
vmid = self.vm_data["vmid"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
@@ -318,7 +320,7 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
self._node_name,
|
||||
self.vm_data["vmid"],
|
||||
vmid,
|
||||
)
|
||||
|
||||
|
||||
@@ -342,8 +344,9 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the container button action via executor."""
|
||||
vmid = self.container_data["vmid"]
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms"):
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
@@ -352,5 +355,5 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
self._node_name,
|
||||
self.container_data["vmid"],
|
||||
vmid,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,13 @@ from .const import PERM_POWER
|
||||
def is_granted(
|
||||
permissions: dict[str, dict[str, int]],
|
||||
p_type: str = "vms",
|
||||
p_id: str | int | None = None, # can be str for nodes
|
||||
permission: str = PERM_POWER,
|
||||
) -> bool:
|
||||
"""Validate user permissions for the given type and permission."""
|
||||
path = f"/{p_type}"
|
||||
return permissions.get(path, {}).get(permission) == 1
|
||||
paths = [f"/{p_type}/{p_id}", f"/{p_type}", "/"]
|
||||
for path in paths:
|
||||
value = permissions.get(path, {}).get(permission)
|
||||
if value is not None:
|
||||
return value == 1
|
||||
return False
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from pyrainbird.async_client import AsyncRainbirdController, CreateController
|
||||
from pyrainbird.async_client import AsyncRainbirdController, create_controller
|
||||
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -26,7 +27,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, TIMEOUT_SECONDS
|
||||
from .coordinator import (
|
||||
RainbirdScheduleUpdateCoordinator,
|
||||
RainbirdUpdateCoordinator,
|
||||
@@ -77,11 +78,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
|
||||
clientsession = async_create_clientsession()
|
||||
_async_register_clientsession_shutdown(hass, entry, clientsession)
|
||||
|
||||
controller = CreateController(
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT_SECONDS):
|
||||
controller = await create_controller(
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except RainbirdAuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except RainbirdApiException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if not (await _async_fix_unique_id(hass, controller, entry)):
|
||||
return False
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyrainbird.async_client import CreateController
|
||||
from pyrainbird.async_client import create_controller
|
||||
from pyrainbird.data import WifiParams
|
||||
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
|
||||
import voluptuous as vol
|
||||
@@ -137,9 +137,9 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
Raises a ConfigFlowError on failure.
|
||||
"""
|
||||
clientsession = async_create_clientsession()
|
||||
controller = CreateController(clientsession, host, password)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT_SECONDS):
|
||||
controller = await create_controller(clientsession, host, password)
|
||||
return await asyncio.gather(
|
||||
controller.get_serial_number(),
|
||||
controller.get_wifi_params(),
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -188,7 +188,9 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = entry_data[CONF_USERNAME]
|
||||
assert self._username
|
||||
self._client = RoborockApiClient(
|
||||
self._username, session=async_get_clientsession(self.hass)
|
||||
self._username,
|
||||
base_url=entry_data[CONF_BASE_URL],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.6.0"]
|
||||
"requirements": ["pysmartthings==3.7.2"]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ async def async_setup_entry(
|
||||
for component in device.status
|
||||
if component in ("cooler", "freezer", "onedoor")
|
||||
and Capability.THERMOSTAT_COOLING_SETPOINT in device.status[component]
|
||||
and device.status[component][Capability.THERMOSTAT_COOLING_SETPOINT][
|
||||
Attribute.COOLING_SETPOINT_RANGE
|
||||
].value
|
||||
is not None
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Support for SLZB-06 buttons."""
|
||||
"""Support for SLZB buttons."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -35,24 +35,25 @@ class SmButtonDescription(ButtonEntityDescription):
|
||||
press_fn: Callable[[CmdWrapper, int], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTONS: list[SmButtonDescription] = [
|
||||
SmButtonDescription(
|
||||
key="core_restart",
|
||||
translation_key="core_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
press_fn=lambda cmd, idx: cmd.reboot(),
|
||||
),
|
||||
CORE_BUTTON = SmButtonDescription(
|
||||
key="core_restart",
|
||||
translation_key="core_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
press_fn=lambda cmd, idx: cmd.reboot(),
|
||||
)
|
||||
|
||||
RADIO_BUTTONS: list[SmButtonDescription] = [
|
||||
SmButtonDescription(
|
||||
key="zigbee_restart",
|
||||
translation_key="zigbee_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
press_fn=lambda cmd, idx: cmd.zb_restart(),
|
||||
press_fn=lambda cmd, idx: cmd.zb_restart(idx=idx),
|
||||
),
|
||||
SmButtonDescription(
|
||||
key="zigbee_flash_mode",
|
||||
translation_key="zigbee_flash_mode",
|
||||
entity_registry_enabled_default=False,
|
||||
press_fn=lambda cmd, idx: cmd.zb_bootloader(),
|
||||
press_fn=lambda cmd, idx: cmd.zb_bootloader(idx=idx),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -73,8 +74,14 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data.data
|
||||
radios = coordinator.data.info.radios
|
||||
|
||||
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
|
||||
entity_created = [False, False]
|
||||
entities = [SmButton(coordinator, CORE_BUTTON)]
|
||||
count = len(radios) if coordinator.data.info.u_device else 1
|
||||
|
||||
for idx in range(count):
|
||||
entities.extend(SmButton(coordinator, button, idx) for button in RADIO_BUTTONS)
|
||||
|
||||
async_add_entities(entities)
|
||||
entity_created = [False] * len(radios)
|
||||
|
||||
@callback
|
||||
def _check_router(startup: bool = False) -> None:
|
||||
@@ -103,7 +110,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class SmButton(SmEntity, ButtonEntity):
|
||||
"""Defines a SLZB-06 button."""
|
||||
"""Defines a SLZB button."""
|
||||
|
||||
coordinator: SmDataUpdateCoordinator
|
||||
entity_description: SmButtonDescription
|
||||
@@ -115,7 +122,7 @@ class SmButton(SmEntity, ButtonEntity):
|
||||
description: SmButtonDescription,
|
||||
idx: int = 0,
|
||||
) -> None:
|
||||
"""Initialize SLZB-06 button entity."""
|
||||
"""Initialize SLZB button entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.16"],
|
||||
"requirements": ["pysmlight==0.3.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -131,7 +131,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
return f"{CLIENT_PREFIX}{host}_{id}"
|
||||
|
||||
@property
|
||||
def _current_group(self) -> Snapgroup:
|
||||
def _current_group(self) -> Snapgroup | None:
|
||||
"""Return the group the client is associated with."""
|
||||
return self._device.group
|
||||
|
||||
@@ -158,12 +158,17 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
if self._device.connected:
|
||||
if self.is_volume_muted or self._current_group.muted:
|
||||
if (
|
||||
self.is_volume_muted
|
||||
or self._current_group is None
|
||||
or self._current_group.muted
|
||||
):
|
||||
return MediaPlayerState.IDLE
|
||||
try:
|
||||
return STREAM_STATUS.get(self._current_group.stream_status)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@property
|
||||
@@ -182,15 +187,31 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""Return the current input source."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
return self._current_group.stream
|
||||
|
||||
@property
|
||||
def source_list(self) -> list[str]:
|
||||
"""List of available input sources."""
|
||||
if self._current_group is None:
|
||||
return []
|
||||
|
||||
return list(self._current_group.streams_by_name().keys())
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Set input source."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="select_source_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source": source,
|
||||
},
|
||||
)
|
||||
|
||||
streams = self._current_group.streams_by_name()
|
||||
if source in streams:
|
||||
await self._current_group.set_stream(streams[source].identifier)
|
||||
@@ -233,6 +254,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def group_members(self) -> list[str] | None:
|
||||
"""List of player entities which are currently grouped together for synchronous playback."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
return [
|
||||
entity_id
|
||||
@@ -248,6 +272,15 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Add `group_members` to this client's current group."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="join_players_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Get the client entity for each group member excluding self
|
||||
entity_registry = er.async_get(self.hass)
|
||||
clients = [
|
||||
@@ -257,27 +290,52 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
and entity.unique_id != self.unique_id
|
||||
]
|
||||
|
||||
# Get unique ID prefix for this host
|
||||
unique_id_prefix = self.get_unique_id(self.coordinator.host_id, "")
|
||||
for client in clients:
|
||||
# Valid entity is a snapcast client
|
||||
# Validate entity is a snapcast client
|
||||
if not client.unique_id.startswith(CLIENT_PREFIX):
|
||||
raise ServiceValidationError(
|
||||
f"Entity '{client.entity_id}' is not a Snapcast client device."
|
||||
)
|
||||
|
||||
# Validate client belongs to the same server
|
||||
if not client.unique_id.startswith(unique_id_prefix):
|
||||
raise ServiceValidationError(
|
||||
f"Entity '{client.entity_id}' does not belong to the same Snapcast server."
|
||||
)
|
||||
|
||||
# Extract client ID and join it to the current group
|
||||
identifier = client.unique_id.split("_")[-1]
|
||||
await self._current_group.add_client(identifier)
|
||||
identifier = client.unique_id.removeprefix(unique_id_prefix)
|
||||
try:
|
||||
await self._current_group.add_client(identifier)
|
||||
except KeyError as e:
|
||||
raise ServiceValidationError(
|
||||
f"Client with identifier '{identifier}' does not exist on the server."
|
||||
) from e
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_unjoin_player(self) -> None:
|
||||
"""Remove this client from it's current group."""
|
||||
"""Remove this client from its current group."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unjoin_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
await self._current_group.remove_client(self._device.identifier)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def metadata(self) -> Mapping[str, Any]:
|
||||
"""Get metadata from the current stream."""
|
||||
if self._current_group is None:
|
||||
return {}
|
||||
|
||||
try:
|
||||
if metadata := self.coordinator.server.stream(
|
||||
self._current_group.stream
|
||||
@@ -341,6 +399,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_position(self) -> int | None:
|
||||
"""Position of current playing media in seconds."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Position is part of properties object, not metadata object
|
||||
if properties := self.coordinator.server.stream(
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"join_players_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to join players."
|
||||
},
|
||||
"select_source_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to select source {source}."
|
||||
},
|
||||
"unjoin_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to unjoin player."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"restore": {
|
||||
"description": "Restores a previously taken snapshot of a media player.",
|
||||
|
||||
@@ -118,7 +118,6 @@ class BrowsableMedia(StrEnum):
|
||||
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
|
||||
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
|
||||
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
|
||||
NEW_RELEASES = "new_releases"
|
||||
|
||||
|
||||
LIBRARY_MAP = {
|
||||
@@ -130,7 +129,6 @@ LIBRARY_MAP = {
|
||||
BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played",
|
||||
BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists",
|
||||
BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks",
|
||||
BrowsableMedia.NEW_RELEASES.value: "New Releases",
|
||||
}
|
||||
|
||||
CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
|
||||
@@ -166,10 +164,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
},
|
||||
BrowsableMedia.NEW_RELEASES.value: {
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.ALBUM,
|
||||
},
|
||||
MediaType.PLAYLIST: {
|
||||
"parent": MediaClass.PLAYLIST,
|
||||
"children": MediaClass.TRACK,
|
||||
@@ -356,14 +350,11 @@ async def build_item_response( # noqa: C901
|
||||
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
|
||||
if top_tracks := await spotify.get_top_tracks():
|
||||
items = [_get_track_item_payload(track) for track in top_tracks]
|
||||
elif media_content_type == BrowsableMedia.NEW_RELEASES:
|
||||
if new_releases := await spotify.get_new_releases():
|
||||
items = [_get_album_item_payload(album) for album in new_releases]
|
||||
elif media_content_type == MediaType.PLAYLIST:
|
||||
if playlist := await spotify.get_playlist(media_content_id):
|
||||
title = playlist.name
|
||||
image = playlist.images[0].url if playlist.images else None
|
||||
for playlist_item in playlist.tracks.items:
|
||||
for playlist_item in playlist.items.items:
|
||||
if playlist_item.track.type is ItemType.TRACK:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(playlist_item.track, Track)
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from spotifyaio import SpotifyClient
|
||||
from spotifyaio import SpotifyClient, SpotifyForbiddenError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
|
||||
@@ -41,6 +41,9 @@ class SpotifyFlowHandler(
|
||||
|
||||
try:
|
||||
current_user = await spotify.get_current_user()
|
||||
except SpotifyForbiddenError:
|
||||
self.logger.exception("User is not subscribed to Spotify")
|
||||
return self.async_abort(reason="user_not_premium")
|
||||
except Exception:
|
||||
self.logger.exception("Error while connecting to Spotify")
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
@@ -11,12 +11,15 @@ from spotifyaio import (
|
||||
Playlist,
|
||||
SpotifyClient,
|
||||
SpotifyConnectionError,
|
||||
SpotifyForbiddenError,
|
||||
SpotifyNotFoundError,
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -33,6 +36,11 @@ type SpotifyConfigEntry = ConfigEntry[SpotifyData]
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
FREE_API_BLOGPOST = (
|
||||
"https://developer.spotify.com/blog/"
|
||||
"2026-02-06-update-on-developer-access-and-platform-security"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpotifyCoordinatorData:
|
||||
@@ -78,6 +86,19 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
|
||||
"""Set up the coordinator."""
|
||||
try:
|
||||
self.current_user = await self.client.get_current_user()
|
||||
except SpotifyForbiddenError as err:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"user_not_premium_{self.config_entry.unique_id}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="user_not_premium",
|
||||
translation_placeholders={"entry_title": self.config_entry.title},
|
||||
learn_more_url=FREE_API_BLOGPOST,
|
||||
)
|
||||
raise ConfigEntryError("User is not subscribed to Spotify") from err
|
||||
except SpotifyConnectionError as err:
|
||||
raise UpdateFailed("Error communicating with Spotify API") from err
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["spotifyaio"],
|
||||
"requirements": ["spotifyaio==1.0.0"]
|
||||
"requirements": ["spotifyaio==2.0.2"]
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ from spotifyaio import (
|
||||
Item,
|
||||
ItemType,
|
||||
PlaybackState,
|
||||
ProductType,
|
||||
RepeatMode as SpotifyRepeatMode,
|
||||
Track,
|
||||
)
|
||||
from spotifyaio.models import ProductType
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -222,7 +222,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
|
||||
if item.type == ItemType.EPISODE:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Episode)
|
||||
return item.show.publisher
|
||||
return item.show.name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Track)
|
||||
@@ -230,12 +230,10 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
|
||||
|
||||
@property
|
||||
@ensure_item
|
||||
def media_album_name(self, item: Item) -> str: # noqa: PLR0206
|
||||
def media_album_name(self, item: Item) -> str | None: # noqa: PLR0206
|
||||
"""Return the media album."""
|
||||
if item.type == ItemType.EPISODE:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Episode)
|
||||
return item.show.name
|
||||
return None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Track)
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_account_mismatch": "The Spotify account authenticated with does not match the account that needed re-authentication.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"user_not_premium": "The Spotify API has been changed and Developer applications created with a free account can no longer access the API. To continue using the Spotify integration, you should use an Spotify Developer application created with a Spotify Premium account, or upgrade to Spotify Premium."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Spotify."
|
||||
@@ -41,6 +42,12 @@
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"user_not_premium": {
|
||||
"description": "[%key:component::spotify::config::abort::user_not_premium%]",
|
||||
"title": "Spotify integration requires a Spotify Premium account"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"api_endpoint_reachable": "Spotify API endpoint reachable"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/starlink",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["starlink-grpc-core==1.2.3"]
|
||||
"requirements": ["starlink-grpc-core==1.2.4"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioswitcher==6.1.0"],
|
||||
"requirements": ["aioswitcher==6.1.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiotedee"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotedee==0.2.25"]
|
||||
"requirements": ["aiotedee==0.2.27"]
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["teltasync==0.1.3"]
|
||||
"requirements": ["teltasync==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -80,6 +80,16 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_ARM_AWAY_ACTION,
|
||||
CONF_ARM_CUSTOM_BYPASS_ACTION,
|
||||
CONF_ARM_HOME_ACTION,
|
||||
CONF_ARM_NIGHT_ACTION,
|
||||
CONF_ARM_VACATION_ACTION,
|
||||
CONF_DISARM_ACTION,
|
||||
CONF_TRIGGER_ACTION,
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Template Alarm Control Panel"
|
||||
|
||||
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
|
||||
@@ -152,6 +162,7 @@ async def async_setup_entry(
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -172,6 +183,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_ALARM_CONTROL_PANELS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_PRESS,)
|
||||
|
||||
BUTTON_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
@@ -66,6 +68,7 @@ async def async_setup_platform(
|
||||
None,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,6 +84,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateButtonEntity,
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,14 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
|
||||
|
||||
CONF_OPEN_AND_CLOSE = "open_or_close"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CLOSE_ACTION,
|
||||
OPEN_ACTION,
|
||||
POSITION_ACTION,
|
||||
STOP_ACTION,
|
||||
TILT_ACTION,
|
||||
)
|
||||
|
||||
TILT_FEATURES = (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
@@ -165,6 +173,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_COVERS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -181,6 +190,7 @@ async def async_setup_entry(
|
||||
StateCoverEntity,
|
||||
COVER_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Fan"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_SET_DIRECTION_ACTION,
|
||||
CONF_SET_OSCILLATING_ACTION,
|
||||
CONF_SET_PERCENTAGE_ACTION,
|
||||
CONF_SET_PRESET_MODE_ACTION,
|
||||
)
|
||||
|
||||
FAN_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
@@ -159,6 +168,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_FANS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -174,6 +184,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateFanEntity,
|
||||
FAN_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -25,7 +26,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers import issue_registry as ir, template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -34,6 +35,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.script import async_validate_actions_config
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -208,6 +210,21 @@ def _format_template(value: Any, field: str | None = None) -> Any:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _get_config_breadcrumbs(config: ConfigType) -> str:
|
||||
"""Try to coerce entity information from the config."""
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
return breadcrumb
|
||||
|
||||
|
||||
def format_migration_config(
|
||||
config: ConfigType | list[ConfigType], depth: int = 0
|
||||
) -> ConfigType | list[ConfigType]:
|
||||
@@ -252,16 +269,7 @@ def create_legacy_template_issue(
|
||||
if domain not in PLATFORMS:
|
||||
return
|
||||
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
|
||||
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
|
||||
|
||||
@@ -296,6 +304,39 @@ def create_legacy_template_issue(
|
||||
)
|
||||
|
||||
|
||||
async def validate_template_scripts(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Validate template scripts."""
|
||||
if not script_options:
|
||||
return
|
||||
|
||||
def _humanize(err: Exception, data: Any) -> str:
|
||||
"""Humanize vol.Invalid, stringify other exceptions."""
|
||||
if isinstance(err, vol.Invalid):
|
||||
return humanize_error(data, err)
|
||||
return str(err)
|
||||
|
||||
breadcrumb: str | None = None
|
||||
for script_option in script_options:
|
||||
if (script_config := config.pop(script_option, None)) is not None:
|
||||
try:
|
||||
config[script_option] = await async_validate_actions_config(
|
||||
hass, script_config
|
||||
)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
if not breadcrumb:
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
_LOGGER.error(
|
||||
"The '%s' actions for %s failed to setup: %s",
|
||||
script_option,
|
||||
breadcrumb,
|
||||
_humanize(err, script_config),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_template_platform(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
@@ -306,6 +347,7 @@ async def async_setup_template_platform(
|
||||
discovery_info: DiscoveryInfoType | None,
|
||||
legacy_fields: dict[str, str] | None = None,
|
||||
legacy_key: str | None = None,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Set up the Template platform."""
|
||||
if discovery_info is None:
|
||||
@@ -337,10 +379,14 @@ async def async_setup_template_platform(
|
||||
# Trigger Configuration
|
||||
if "coordinator" in discovery_info:
|
||||
if trigger_entity_cls:
|
||||
entities = [
|
||||
trigger_entity_cls(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
]
|
||||
entities = []
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
entities.append(
|
||||
trigger_entity_cls(
|
||||
hass, discovery_info["coordinator"], entity_config
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
else:
|
||||
raise PlatformNotReady(
|
||||
@@ -349,6 +395,9 @@ async def async_setup_template_platform(
|
||||
return
|
||||
|
||||
# Modern Configuration
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
|
||||
async_create_template_tracking_entities(
|
||||
state_entity_cls,
|
||||
async_add_entities,
|
||||
@@ -365,6 +414,7 @@ async def async_setup_template_entry(
|
||||
state_entity_cls: type[TemplateEntity],
|
||||
config_schema: vol.Schema | vol.All,
|
||||
replace_value_template: bool = False,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Setup the Template from a config entry."""
|
||||
options = dict(config_entry.options)
|
||||
@@ -377,6 +427,7 @@ async def async_setup_template_entry(
|
||||
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = config_schema(options)
|
||||
await validate_template_scripts(hass, validated_config, script_options)
|
||||
|
||||
async_add_entities(
|
||||
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
||||
|
||||
@@ -129,6 +129,18 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_EFFECT_ACTION,
|
||||
CONF_HS_ACTION,
|
||||
CONF_LEVEL_ACTION,
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_RGB_ACTION,
|
||||
CONF_RGBW_ACTION,
|
||||
CONF_RGBWW_ACTION,
|
||||
CONF_TEMPERATURE_ACTION,
|
||||
)
|
||||
|
||||
LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
@@ -142,8 +154,6 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -226,6 +236,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_LIGHTS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -242,6 +253,7 @@ async def async_setup_entry(
|
||||
StateLightEntity,
|
||||
LIGHT_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_LOCK,
|
||||
CONF_OPEN,
|
||||
CONF_UNLOCK,
|
||||
)
|
||||
|
||||
|
||||
LOCK_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
@@ -112,6 +119,7 @@ async def async_setup_platform(
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -127,6 +135,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateLockEntity,
|
||||
LOCK_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SET_VALUE,)
|
||||
|
||||
NUMBER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
@@ -81,6 +83,7 @@ async def async_setup_platform(
|
||||
TriggerNumberEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,6 +99,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateNumberEntity,
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ CONF_SELECT_OPTION = "select_option"
|
||||
|
||||
DEFAULT_NAME = "Template Select"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
|
||||
|
||||
SELECT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
@@ -79,6 +81,7 @@ async def async_setup_platform(
|
||||
TriggerSelectEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +97,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateSelect,
|
||||
SELECT_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -57,11 +57,16 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_TURN_OFF,
|
||||
CONF_TURN_ON,
|
||||
)
|
||||
|
||||
SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -109,6 +114,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_SWITCHES,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -125,6 +131,7 @@ async def async_setup_entry(
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -266,13 +266,20 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
|
||||
def _get_this_variable(self) -> TemplateStateFromEntityId:
|
||||
"""Create a this variable for the entity."""
|
||||
entity_id = self.entity_id
|
||||
if self._preview_callback:
|
||||
preview_entity_id = async_generate_entity_id(
|
||||
self._entity_id_format, self._attr_name or "preview", hass=self.hass
|
||||
)
|
||||
return TemplateStateFromEntityId(self.hass, preview_entity_id)
|
||||
# During config flow, the registry entry and entity_id will be None. In this scenario,
|
||||
# a temporary entity_id is created.
|
||||
# During option flow, the preview entity_id will be None, however the registry entry
|
||||
# will contain the target entity_id.
|
||||
if self.registry_entry:
|
||||
entity_id = self.registry_entry.entity_id
|
||||
else:
|
||||
entity_id = async_generate_entity_id(
|
||||
self._entity_id_format, self._attr_name or "preview", hass=self.hass
|
||||
)
|
||||
|
||||
return TemplateStateFromEntityId(self.hass, self.entity_id)
|
||||
return TemplateStateFromEntityId(self.hass, entity_id)
|
||||
|
||||
def _render_script_variables(self) -> dict[str, Any]:
|
||||
"""Render configured variables."""
|
||||
|
||||
@@ -65,6 +65,8 @@ CONF_SPECIFIC_VERSION = "specific_version"
|
||||
CONF_TITLE = "title"
|
||||
CONF_UPDATE_PERCENTAGE = "update_percentage"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_INSTALL,)
|
||||
|
||||
UPDATE_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
|
||||
@@ -105,6 +107,7 @@ async def async_setup_platform(
|
||||
TriggerUpdateEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,6 +123,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateUpdateEntity,
|
||||
UPDATE_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,16 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
)
|
||||
|
||||
VACUUM_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
@@ -150,6 +160,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_VACUUMS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -165,6 +176,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateStateVacuumEntity,
|
||||
VACUUM_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, TypedDict, cast
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
import tibber
|
||||
@@ -38,6 +39,58 @@ FIVE_YEARS = 5 * 365 * 24
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TibberHomeData(TypedDict):
|
||||
"""Data for a Tibber home used by the price sensor."""
|
||||
|
||||
currency: str
|
||||
price_unit: str
|
||||
current_price: float | None
|
||||
current_price_time: datetime | None
|
||||
intraday_price_ranking: float | None
|
||||
max_price: float
|
||||
avg_price: float
|
||||
min_price: float
|
||||
off_peak_1: float
|
||||
peak: float
|
||||
off_peak_2: float
|
||||
month_cost: float | None
|
||||
peak_hour: float | None
|
||||
peak_hour_time: datetime | None
|
||||
month_cons: float | None
|
||||
app_nickname: str | None
|
||||
grid_company: str | None
|
||||
estimated_annual_consumption: int | None
|
||||
|
||||
|
||||
def _build_home_data(home: tibber.TibberHome) -> TibberHomeData:
|
||||
"""Build TibberHomeData from a TibberHome for the price sensor."""
|
||||
current_price, last_updated, price_rank = home.current_price_data()
|
||||
attributes = home.current_attributes()
|
||||
result: TibberHomeData = {
|
||||
"currency": home.currency,
|
||||
"price_unit": home.price_unit,
|
||||
"current_price": current_price,
|
||||
"current_price_time": last_updated,
|
||||
"intraday_price_ranking": price_rank,
|
||||
"max_price": attributes["max_price"],
|
||||
"avg_price": attributes["avg_price"],
|
||||
"min_price": attributes["min_price"],
|
||||
"off_peak_1": attributes["off_peak_1"],
|
||||
"peak": attributes["peak"],
|
||||
"off_peak_2": attributes["off_peak_2"],
|
||||
"month_cost": home.month_cost,
|
||||
"peak_hour": home.peak_hour,
|
||||
"peak_hour_time": home.peak_hour_time,
|
||||
"month_cons": home.month_cons,
|
||||
"app_nickname": home.info["viewer"]["home"].get("appNickname"),
|
||||
"grid_company": home.info["viewer"]["home"]["meteringPointData"]["gridCompany"],
|
||||
"estimated_annual_consumption": home.info["viewer"]["home"][
|
||||
"meteringPointData"
|
||||
]["estimatedAnnualConsumption"],
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Handle Tibber data and insert statistics."""
|
||||
|
||||
@@ -57,13 +110,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
name=f"Tibber {tibber_connection.name}",
|
||||
update_interval=timedelta(minutes=20),
|
||||
)
|
||||
self._tibber_connection = tibber_connection
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data via API."""
|
||||
tibber_connection = await self.config_entry.runtime_data.async_get_client(
|
||||
self.hass
|
||||
)
|
||||
|
||||
try:
|
||||
await self._tibber_connection.fetch_consumption_data_active_homes()
|
||||
await self._tibber_connection.fetch_production_data_active_homes()
|
||||
await tibber_connection.fetch_consumption_data_active_homes()
|
||||
await tibber_connection.fetch_production_data_active_homes()
|
||||
await self._insert_statistics()
|
||||
except tibber.RetryableHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
@@ -75,7 +131,10 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
async def _insert_statistics(self) -> None:
|
||||
"""Insert Tibber statistics."""
|
||||
for home in self._tibber_connection.get_homes():
|
||||
tibber_connection = await self.config_entry.runtime_data.async_get_client(
|
||||
self.hass
|
||||
)
|
||||
for home in tibber_connection.get_homes():
|
||||
sensors: list[tuple[str, bool, str | None, str]] = []
|
||||
if home.hourly_consumption_data:
|
||||
sensors.append(
|
||||
@@ -194,6 +253,76 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
async_add_external_statistics(self.hass, metadata, statistics)
|
||||
|
||||
|
||||
class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
|
||||
"""Handle Tibber price data and insert statistics."""
|
||||
|
||||
config_entry: TibberConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: TibberConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the price coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN} price",
|
||||
update_interval=timedelta(minutes=1),
|
||||
)
|
||||
|
||||
def _seconds_until_next_15_minute(self) -> float:
|
||||
"""Return seconds until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
|
||||
now = dt_util.utcnow()
|
||||
next_minute = ((now.minute // 15) + 1) * 15
|
||||
if next_minute >= 60:
|
||||
next_run = now.replace(minute=0, second=0, microsecond=0) + timedelta(
|
||||
hours=1
|
||||
)
|
||||
else:
|
||||
next_run = now.replace(
|
||||
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
|
||||
)
|
||||
return (next_run - now).total_seconds()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, TibberHomeData]:
|
||||
"""Update data via API and return per-home data for sensors."""
|
||||
tibber_connection = await self.config_entry.runtime_data.async_get_client(
|
||||
self.hass
|
||||
)
|
||||
active_homes = tibber_connection.get_homes(only_active=True)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
tibber_connection.fetch_consumption_data_active_homes(),
|
||||
tibber_connection.fetch_production_data_active_homes(),
|
||||
)
|
||||
|
||||
now = dt_util.now()
|
||||
homes_to_update = [
|
||||
home
|
||||
for home in active_homes
|
||||
if (
|
||||
(last_data_timestamp := home.last_data_timestamp) is None
|
||||
or (last_data_timestamp - now).total_seconds() < 11 * 3600
|
||||
)
|
||||
]
|
||||
|
||||
if homes_to_update:
|
||||
await asyncio.gather(
|
||||
*(home.update_info_and_price_info() for home in homes_to_update)
|
||||
)
|
||||
except tibber.RetryableHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
except tibber.FatalHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
|
||||
result = {home.home_id: _build_home_data(home) for home in active_homes}
|
||||
|
||||
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
|
||||
return result
|
||||
|
||||
|
||||
class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
|
||||
"""Fetch and cache Tibber Data API device capabilities."""
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from random import randrange
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
@@ -42,18 +40,20 @@ from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.util import Throttle, dt as dt_util
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
|
||||
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
|
||||
from .coordinator import (
|
||||
TibberDataAPICoordinator,
|
||||
TibberDataCoordinator,
|
||||
TibberPriceCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = "mdi:currency-usd"
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
PARALLEL_UPDATES = 0
|
||||
TWENTY_MINUTES = 20 * 60
|
||||
|
||||
RT_SENSORS_UNIQUE_ID_MIGRATION = {
|
||||
"accumulated_consumption_last_hour": "accumulated consumption current hour",
|
||||
@@ -610,6 +610,7 @@ async def _async_setup_graphql_sensors(
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
coordinator: TibberDataCoordinator | None = None
|
||||
price_coordinator: TibberPriceCoordinator | None = None
|
||||
entities: list[TibberSensor] = []
|
||||
for home in tibber_connection.get_homes(only_active=False):
|
||||
try:
|
||||
@@ -626,7 +627,9 @@ async def _async_setup_graphql_sensors(
|
||||
raise PlatformNotReady from err
|
||||
|
||||
if home.has_active_subscription:
|
||||
entities.append(TibberSensorElPrice(home))
|
||||
if price_coordinator is None:
|
||||
price_coordinator = TibberPriceCoordinator(hass, entry)
|
||||
entities.append(TibberSensorElPrice(price_coordinator, home))
|
||||
if coordinator is None:
|
||||
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
|
||||
entities.extend(
|
||||
@@ -737,19 +740,21 @@ class TibberSensor(SensorEntity):
|
||||
return device_info
|
||||
|
||||
|
||||
class TibberSensorElPrice(TibberSensor):
|
||||
class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator]):
|
||||
"""Representation of a Tibber sensor for el price."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_translation_key = "electricity_price"
|
||||
|
||||
def __init__(self, tibber_home: TibberHome) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TibberPriceCoordinator,
|
||||
tibber_home: TibberHome,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(tibber_home=tibber_home)
|
||||
self._last_updated: datetime.datetime | None = None
|
||||
self._spread_load_constant = randrange(TWENTY_MINUTES)
|
||||
|
||||
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
|
||||
self._attr_available = False
|
||||
self._attr_native_unit_of_measurement = tibber_home.price_unit
|
||||
self._attr_extra_state_attributes = {
|
||||
"app_nickname": None,
|
||||
"grid_company": None,
|
||||
@@ -768,51 +773,38 @@ class TibberSensorElPrice(TibberSensor):
|
||||
|
||||
self._device_name = self._home_name
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data and updates the states."""
|
||||
now = dt_util.now()
|
||||
if (
|
||||
not self._tibber_home.last_data_timestamp
|
||||
or (self._tibber_home.last_data_timestamp - now).total_seconds()
|
||||
< 10 * 3600 - self._spread_load_constant
|
||||
or not self.available
|
||||
):
|
||||
_LOGGER.debug("Asking for new data")
|
||||
await self._fetch_data()
|
||||
|
||||
elif (
|
||||
self._tibber_home.price_total
|
||||
and self._last_updated
|
||||
and self._last_updated.hour == now.hour
|
||||
and now - self._last_updated < timedelta(minutes=15)
|
||||
and self._tibber_home.last_data_timestamp
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
data = self.coordinator.data
|
||||
if not data or (
|
||||
(home_data := data.get(self._tibber_home.home_id)) is None
|
||||
or (current_price := home_data.get("current_price")) is None
|
||||
):
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
res = self._tibber_home.current_price_data()
|
||||
self._attr_native_value, self._last_updated, price_rank = res
|
||||
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
|
||||
|
||||
attrs = self._tibber_home.current_attributes()
|
||||
self._attr_extra_state_attributes.update(attrs)
|
||||
self._attr_available = self._attr_native_value is not None
|
||||
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def _fetch_data(self) -> None:
|
||||
_LOGGER.debug("Fetching data")
|
||||
try:
|
||||
await self._tibber_home.update_info_and_price_info()
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
return
|
||||
data = self._tibber_home.info["viewer"]["home"]
|
||||
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
|
||||
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
|
||||
"gridCompany"
|
||||
self._attr_native_unit_of_measurement = home_data.get(
|
||||
"price_unit", self._tibber_home.price_unit
|
||||
)
|
||||
self._attr_native_value = current_price
|
||||
self._attr_extra_state_attributes["intraday_price_ranking"] = home_data.get(
|
||||
"intraday_price_ranking"
|
||||
)
|
||||
self._attr_extra_state_attributes["max_price"] = home_data["max_price"]
|
||||
self._attr_extra_state_attributes["avg_price"] = home_data["avg_price"]
|
||||
self._attr_extra_state_attributes["min_price"] = home_data["min_price"]
|
||||
self._attr_extra_state_attributes["off_peak_1"] = home_data["off_peak_1"]
|
||||
self._attr_extra_state_attributes["peak"] = home_data["peak"]
|
||||
self._attr_extra_state_attributes["off_peak_2"] = home_data["off_peak_2"]
|
||||
self._attr_extra_state_attributes["app_nickname"] = home_data["app_nickname"]
|
||||
self._attr_extra_state_attributes["grid_company"] = home_data["grid_company"]
|
||||
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
|
||||
"estimated_annual_consumption"
|
||||
]
|
||||
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
|
||||
"meteringPointData"
|
||||
]["estimatedAnnualConsumption"]
|
||||
self._attr_available = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["tplink-omada-client==1.5.3"]
|
||||
"requirements": ["tplink-omada-client==1.5.6"]
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ clean_area:
|
||||
selector:
|
||||
area:
|
||||
multiple: true
|
||||
reorder: true
|
||||
|
||||
send_command:
|
||||
target:
|
||||
|
||||
@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
Keys.WARNING: VictronBLESensorEntityDescription(
|
||||
key=Keys.WARNING,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="alarm",
|
||||
translation_key="warning",
|
||||
options=ALARM_OPTIONS,
|
||||
),
|
||||
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
|
||||
@@ -410,7 +410,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
),
|
||||
}
|
||||
|
||||
for i in range(1, 8):
|
||||
for i in range(1, 9):
|
||||
cell_key = getattr(Keys, f"CELL_{i}_VOLTAGE")
|
||||
SENSOR_DESCRIPTIONS[cell_key] = VictronBLESensorEntityDescription(
|
||||
key=cell_key,
|
||||
@@ -418,6 +418,7 @@ for i in range(1, 8):
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_placeholders={"cell": str(i)},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -248,7 +248,24 @@
|
||||
"name": "[%key:component::victron_ble::common::starter_voltage%]"
|
||||
},
|
||||
"warning": {
|
||||
"name": "Warning"
|
||||
"name": "Warning",
|
||||
"state": {
|
||||
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
|
||||
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
|
||||
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
|
||||
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
|
||||
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
|
||||
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
|
||||
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
|
||||
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
|
||||
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
|
||||
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
|
||||
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
|
||||
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
|
||||
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
|
||||
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
|
||||
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
|
||||
}
|
||||
},
|
||||
"yield_today": {
|
||||
"name": "Yield today"
|
||||
|
||||
@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
data,
|
||||
session,
|
||||
)
|
||||
self._session = session
|
||||
|
||||
# Last resort as no MAC or S/N can be retrieved via API
|
||||
self._id = config_entry.unique_id
|
||||
@@ -135,11 +136,15 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
|
||||
|
||||
try:
|
||||
await self.api.login()
|
||||
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
|
||||
_LOGGER.debug(
|
||||
"Session cookies missing for host %s, re-login",
|
||||
self.api.base_url.host,
|
||||
)
|
||||
await self.api.login()
|
||||
raw_data_devices = await self.api.get_devices_data()
|
||||
data_sensors = await self.api.get_sensor_data()
|
||||
data_wifi = await self.api.get_wifi_data()
|
||||
await self.api.logout()
|
||||
except exceptions.CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiovodafone"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiovodafone==3.1.2"]
|
||||
"requirements": ["aiovodafone==3.1.3"]
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
|
||||
await self.coordinator.api.set_wifi_status(
|
||||
status, self.entity_description.typology, self.entity_description.band
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
except CannotAuthenticate as err:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user