forked from home-assistant/core
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c09f15b0e9 | ||
|
|
9a44d668d6 | ||
|
|
67e0197a7a | ||
|
|
a5a8cfa17d | ||
|
|
60c3e701e9 | ||
|
|
b9b129dcf5 | ||
|
|
d882ab236a | ||
|
|
140cc0e486 | ||
|
|
6ac7c0f893 | ||
|
|
096d50617f | ||
|
|
9dd8c0cc4f | ||
|
|
de0fab86ec | ||
|
|
ada837ee95 | ||
|
|
67e73173f6 | ||
|
|
029411d3fa | ||
|
|
6ba033f934 | ||
|
|
336742e335 | ||
|
|
4a94430bf0 | ||
|
|
cc337f7b1e | ||
|
|
515771553f | ||
|
|
e204812d2b | ||
|
|
ca703cb858 | ||
|
|
b018d4a97d | ||
|
|
146768ff8a | ||
|
|
ea7473ed67 | ||
|
|
0e8393766f | ||
|
|
7d2536c503 | ||
|
|
f9cbf1b30c | ||
|
|
d66d87d271 | ||
|
|
5a8fa6cf38 | ||
|
|
76340035db | ||
|
|
0a26e68d0c | ||
|
|
14127b910f | ||
|
|
ba4d081021 | ||
|
|
18d65d513e | ||
|
|
9176994947 | ||
|
|
d389b55f40 | ||
|
|
0ccff9fc54 | ||
|
|
a8836ca7b6 | ||
|
|
f5d04a970f | ||
|
|
7aec98dafd | ||
|
|
773564d4f5 | ||
|
|
3e2edc1a2d | ||
|
|
9cfc9b9baf | ||
|
|
92b67ead83 | ||
|
|
ee9525cc00 | ||
|
|
571bfaf5d7 | ||
|
|
a3475607b2 | ||
|
|
f0a653d010 | ||
|
|
eecdf66013 | ||
|
|
f99db05a4a | ||
|
|
635731421f | ||
|
|
44743df7d6 | ||
|
|
33617694cc | ||
|
|
ed445d20b9 | ||
|
|
66c2fe091b | ||
|
|
8c80f47a35 | ||
|
|
e37025c1c7 | ||
|
|
0aabde081b | ||
|
|
a1c9d53474 | ||
|
|
094996ad0c | ||
|
|
ce359a7689 | ||
|
|
ee599160b3 | ||
|
|
dd076f7a13 | ||
|
|
3021d38b6f | ||
|
|
bfcabeaf26 | ||
|
|
c31e0336dc | ||
|
|
a1e42cac7a | ||
|
|
14a3e5b771 | ||
|
|
5901c543da | ||
|
|
456b80e6ae | ||
|
|
e5644ae011 | ||
|
|
41c794c733 | ||
|
|
a481448d46 | ||
|
|
2bd7ce618a | ||
|
|
da1ac4f1e9 | ||
|
|
f0cb638106 | ||
|
|
dad2396d01 | ||
|
|
91e4d8b663 | ||
|
|
e35496133e | ||
|
|
3be808ae1e | ||
|
|
c5772916a1 | ||
|
|
8cd63b80b1 | ||
|
|
c087654386 | ||
|
|
60b9e65c78 | ||
|
|
79b304a5d2 | ||
|
|
bb9fd126e5 | ||
|
|
bff2d5c26c | ||
|
|
46d9ac8380 | ||
|
|
5da3ca4bb1 | ||
|
|
2c99fdc092 | ||
|
|
31a075fb13 | ||
|
|
1d132d7a1e | ||
|
|
3b6f88cfa7 | ||
|
|
b927763d8d | ||
|
|
d00e1cb6a5 | ||
|
|
adf7474edb | ||
|
|
041d663cb8 | ||
|
|
37f611a8d3 | ||
|
|
be99329efa | ||
|
|
327cb70bb8 | ||
|
|
be2b5a4c3a | ||
|
|
d1eda9dd73 | ||
|
|
b902cb5a13 | ||
|
|
1184ee4a59 | ||
|
|
2cf898afcc | ||
|
|
df53e19eda | ||
|
|
7f79b26341 | ||
|
|
2cbf53ad7b | ||
|
|
c52607b465 | ||
|
|
087566072d | ||
|
|
6b814afd39 | ||
|
|
ea8aa6b07d | ||
|
|
1b0f731e30 | ||
|
|
1ebde4a880 | ||
|
|
e53bd477b4 | ||
|
|
3f9287c36b | ||
|
|
b2b940fc32 | ||
|
|
7d9e170512 | ||
|
|
6ab92abe80 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 10
|
||||
CACHE_VERSION: 11
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2024.10"
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.9.0"],
|
||||
"requirements": ["airgradient==0.9.1"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
|
||||
OperationMode.HEATING: HVACMode.HEAT,
|
||||
OperationMode.FAN: HVACMode.FAN_ONLY,
|
||||
OperationMode.DRY: HVACMode.DRY,
|
||||
OperationMode.AUX_HEATING: HVACMode.HEAT,
|
||||
OperationMode.AUTO: HVACMode.HEAT_COOL,
|
||||
}
|
||||
HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = {
|
||||
@@ -157,9 +158,10 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
|
||||
self.get_airzone_value(AZD_TEMP_UNIT)
|
||||
]
|
||||
self._attr_hvac_modes = [
|
||||
_attr_hvac_modes = [
|
||||
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
|
||||
]
|
||||
self._attr_hvac_modes = list(dict.fromkeys(_attr_hvac_modes))
|
||||
if (
|
||||
self.get_airzone_value(AZD_SPEED) is not None
|
||||
and self.get_airzone_value(AZD_SPEEDS) is not None
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.9.3"]
|
||||
"requirements": ["aioairzone==0.9.5"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.5"]
|
||||
"requirements": ["aioairzone-cloud==0.6.6"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully connected to AlarmDecoder."
|
||||
@@ -37,7 +38,7 @@
|
||||
"title": "Configure AlarmDecoder",
|
||||
"description": "What would you like to edit?",
|
||||
"data": {
|
||||
"edit_select": "Edit"
|
||||
"edit_selection": "Edit"
|
||||
}
|
||||
},
|
||||
"arm_settings": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"site": {
|
||||
"data": {
|
||||
"site_nmi": "Site NMI",
|
||||
"site_id": "Site NMI",
|
||||
"site_name": "Site Name"
|
||||
},
|
||||
"description": "Select the NMI of the site you would like to add"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "You must select at least one integration to track"
|
||||
"no_integrations_selected": "You must select at least one integration to track"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]"
|
||||
"no_integrations_selected": "[%key:component::analytics_insights::config::error::no_integrations_selected%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -4,6 +4,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_THRESHOLD, DEFAULT_THRESHOLD
|
||||
from .coordinator import AuroraDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
@@ -21,9 +22,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: AuroraConfigEntry) -> bo
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: AuroraConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
entry.runtime_data.threshold = int(
|
||||
entry.options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD)
|
||||
)
|
||||
# refresh the state of the visibility alert binary sensor
|
||||
await entry.runtime_data.async_request_refresh()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AuroraConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -38,8 +38,8 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator[int]):
|
||||
)
|
||||
|
||||
self.api = AuroraForecast(async_get_clientsession(hass))
|
||||
self.latitude = int(self.config_entry.data[CONF_LATITUDE])
|
||||
self.longitude = int(self.config_entry.data[CONF_LONGITUDE])
|
||||
self.latitude = round(self.config_entry.data[CONF_LATITUDE])
|
||||
self.longitude = round(self.config_entry.data[CONF_LONGITUDE])
|
||||
self.threshold = int(
|
||||
self.config_entry.options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD)
|
||||
)
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "Threshold (%)"
|
||||
"forecast_threshold": "Threshold (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
"name": "Awair",
|
||||
"codeowners": ["@ahayworth", "@danielsjf"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"macaddress": "70886B1*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/awair",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["python_awair"],
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"init": {
|
||||
"title": "Options for the Azure Event Hub.",
|
||||
"data": {
|
||||
"send_interval": "Interval between sending batches to the hub."
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"description": "Set up your BleBox to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::ip%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"title": "Set up your BleBox device"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==1.0.2"],
|
||||
"requirements": ["pyblu==1.0.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
||||
@@ -493,6 +493,8 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
position = self._status.seconds
|
||||
if position is None:
|
||||
return None
|
||||
|
||||
if mediastate == MediaPlayerState.PLAYING:
|
||||
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"reconfigure_confirm": {
|
||||
"data": {
|
||||
"filename": "[%key:component::bryant_evolution::config::step::user::data::filename%]"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
ssl_verify_cert=entry.data[CONF_VERIFY_SSL],
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
try:
|
||||
await hass.async_add_executor_job(client.principal)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiostreammagic"],
|
||||
"requirements": ["aiostreammagic==2.3.1"],
|
||||
"requirements": ["aiostreammagic==2.5.0"],
|
||||
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.2"],
|
||||
"requirements": ["PyChromecast==14.0.3"],
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
||||
93
homeassistant/components/comelit/diagnostics.py
Normal file
93
homeassistant/components/comelit/diagnostics.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Diagnostics support for Comelit integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import (
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit.const import BRIDGE
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PIN, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ComelitBaseCoordinator
|
||||
|
||||
TO_REDACT = {CONF_PIN}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
coordinator: ComelitBaseCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
dev_list: list[dict[str, Any]] = []
|
||||
dev_type_list: list[dict[int, Any]] = []
|
||||
|
||||
for dev_type in coordinator.data:
|
||||
dev_type_list = []
|
||||
for sensor_data in coordinator.data[dev_type].values():
|
||||
if isinstance(sensor_data, ComelitSerialBridgeObject):
|
||||
dev_type_list.append(
|
||||
{
|
||||
sensor_data.index: {
|
||||
"name": sensor_data.name,
|
||||
"status": sensor_data.status,
|
||||
"human_status": sensor_data.human_status,
|
||||
"protected": sensor_data.protected,
|
||||
"val": sensor_data.val,
|
||||
"zone": sensor_data.zone,
|
||||
"power": sensor_data.power,
|
||||
"power_unit": sensor_data.power_unit,
|
||||
}
|
||||
}
|
||||
)
|
||||
if isinstance(sensor_data, ComelitVedoAreaObject):
|
||||
dev_type_list.append(
|
||||
{
|
||||
sensor_data.index: {
|
||||
"name": sensor_data.name,
|
||||
"human_status": sensor_data.human_status.value,
|
||||
"p1": sensor_data.p1,
|
||||
"p2": sensor_data.p2,
|
||||
"ready": sensor_data.ready,
|
||||
"armed": sensor_data.armed,
|
||||
"alarm": sensor_data.alarm,
|
||||
"alarm_memory": sensor_data.alarm_memory,
|
||||
"sabotage": sensor_data.sabotage,
|
||||
"anomaly": sensor_data.anomaly,
|
||||
"in_time": sensor_data.in_time,
|
||||
"out_time": sensor_data.out_time,
|
||||
}
|
||||
}
|
||||
)
|
||||
if isinstance(sensor_data, ComelitVedoZoneObject):
|
||||
dev_type_list.append(
|
||||
{
|
||||
sensor_data.index: {
|
||||
"name": sensor_data.name,
|
||||
"human_status": sensor_data.human_status.value,
|
||||
"status": sensor_data.status,
|
||||
"status_api": sensor_data.status_api,
|
||||
}
|
||||
}
|
||||
)
|
||||
dev_list.append({dev_type: dev_type_list})
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"type": entry.data.get(CONF_TYPE, BRIDGE),
|
||||
"device_info": {
|
||||
"last_update success": coordinator.last_update_success,
|
||||
"last_exception": repr(coordinator.last_exception),
|
||||
"devices": dev_list,
|
||||
},
|
||||
}
|
||||
@@ -159,6 +159,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
|
||||
|
||||
if values:
|
||||
await self.device.set(values)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
@@ -261,6 +262,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
|
||||
await self.device.set_advanced_mode(
|
||||
HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
@@ -275,9 +277,11 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn device on."""
|
||||
await self.device.set({})
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn device off."""
|
||||
await self.device.set(
|
||||
{HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]}
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -63,10 +63,12 @@ class DaikinZoneSwitch(DaikinEntity, SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone on."""
|
||||
await self.device.set_zone(self._zone_id, "zone_onoff", "1")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone off."""
|
||||
await self.device.set_zone(self._zone_id, "zone_onoff", "0")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
|
||||
@@ -88,10 +90,12 @@ class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone on."""
|
||||
await self.device.set_streamer("on")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone off."""
|
||||
await self.device.set_streamer("off")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
|
||||
@@ -112,7 +116,9 @@ class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone on."""
|
||||
await self.device.set({})
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone off."""
|
||||
await self.device.set({DAIKIN_ATTR_MODE: "off"})
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -51,7 +51,7 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
tracked.add(station.mac_address)
|
||||
async_add_entities(new_entities)
|
||||
async_add_entities(new_entities)
|
||||
|
||||
@callback
|
||||
def restore_entities() -> None:
|
||||
|
||||
@@ -9,6 +9,7 @@ from devolo_plc_api.device_api import (
|
||||
)
|
||||
from devolo_plc_api.plcnet_api import DataRate, LogicalNetwork
|
||||
|
||||
from homeassistant.const import ATTR_CONNECTIONS
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
@@ -45,7 +46,6 @@ class DevoloEntity(Entity):
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"http://{self.device.ip}",
|
||||
connections={(CONNECTION_NETWORK_MAC, self.device.mac)},
|
||||
identifiers={(DOMAIN, str(self.device.serial_number))},
|
||||
manufacturer="devolo",
|
||||
model=self.device.product,
|
||||
@@ -53,6 +53,10 @@ class DevoloEntity(Entity):
|
||||
serial_number=self.device.serial_number,
|
||||
sw_version=self.device.firmware_version,
|
||||
)
|
||||
if self.device.mac:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_NETWORK_MAC, self.device.mac)
|
||||
}
|
||||
self._attr_translation_key = self.entity_description.key
|
||||
self._attr_unique_id = (
|
||||
f"{self.device.serial_number}_{self.entity_description.key}"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==3.0.2"],
|
||||
"requirements": ["DoorBirdPy==3.0.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_axis-video._tcp.local.",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyDuotecno==2024.9.0"]
|
||||
"requirements": ["pyDuotecno==2024.10.1"]
|
||||
}
|
||||
|
||||
@@ -5,18 +5,21 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Duotecno device."
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"],
|
||||
"requirements": ["pyeconet==0.1.22"]
|
||||
"requirements": ["pyeconet==0.1.23"]
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
config[DOMAIN][CONF_PASSWORD],
|
||||
)
|
||||
|
||||
except evo.AuthenticationFailed as err:
|
||||
except (evo.AuthenticationFailed, evo.RequestFailed) as err:
|
||||
handle_evo_exception(err)
|
||||
return False
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20241002.2"]
|
||||
"requirements": ["home-assistant-frontend==20241002.4"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ DOMAIN = "fujitsu_fglair"
|
||||
|
||||
CONF_REGION = "region"
|
||||
CONF_EUROPE = "is_europe"
|
||||
REGION_EU = "EU"
|
||||
REGION_EU = "eu"
|
||||
REGION_DEFAULT = "default"
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["ayla-iot-unofficial==1.4.1"]
|
||||
"requirements": ["ayla-iot-unofficial==1.4.2"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["fyta_cli==0.6.6"]
|
||||
"requirements": ["fyta_cli==0.6.7"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==6.1.5", "oauth2client==4.1.3", "ical==8.2.0"]
|
||||
"requirements": ["gcal-sync==6.1.6", "oauth2client==4.1.3", "ical==8.2.0"]
|
||||
}
|
||||
|
||||
@@ -172,10 +172,12 @@ class BaseGoogleCloudProvider:
|
||||
_LOGGER.error("Error: %s when validating options: %s", err, options)
|
||||
return None, None
|
||||
|
||||
encoding = texttospeech.AudioEncoding(options[CONF_ENCODING])
|
||||
gender: texttospeech.SsmlVoiceGender | None = texttospeech.SsmlVoiceGender(
|
||||
encoding: texttospeech.AudioEncoding = texttospeech.AudioEncoding[
|
||||
options[CONF_ENCODING]
|
||||
] # type: ignore[misc]
|
||||
gender: texttospeech.SsmlVoiceGender | None = texttospeech.SsmlVoiceGender[
|
||||
options[CONF_GENDER]
|
||||
)
|
||||
] # type: ignore[misc]
|
||||
voice = options[CONF_VOICE]
|
||||
if voice:
|
||||
gender = None
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"wrong_account": "Wrong account: Please authenticate with the right account.",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"wrong_account": "Wrong account: Please authenticate with the right account.",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
||||
@@ -14,6 +14,9 @@ from homeassistant.util import dt as dt_util
|
||||
def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None:
|
||||
"""Calculate due date for dailies and yesterdailies."""
|
||||
|
||||
if task["everyX"] == 0 or not task.get("nextDue"): # grey dailies never become due
|
||||
return None
|
||||
|
||||
today = to_date(last_cron)
|
||||
startdate = to_date(task["startDate"])
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"cmd": {
|
||||
"name": "Command",
|
||||
"description": "Command itself. Could be decimal number or string with hexadeximal notation: \"0x10\"."
|
||||
"description": "Command itself. Could be decimal number or string with hexadecimal notation: \"0x10\"."
|
||||
},
|
||||
"dst": {
|
||||
"name": "Destination",
|
||||
"description": "Destination for command. Could be decimal number or string with hexadeximal notation: \"0x10\"."
|
||||
"description": "Destination for command. Could be decimal number or string with hexadecimal notation: \"0x10\"."
|
||||
},
|
||||
"raw": {
|
||||
"name": "Raw",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"src": {
|
||||
"name": "Source",
|
||||
"description": "Source of command. Could be decimal number or string with hexadeximal notation: \"0x10\"."
|
||||
"description": "Source of command. Could be decimal number or string with hexadecimal notation: \"0x10\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.57", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.58", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ from .const import (
|
||||
RETRY,
|
||||
)
|
||||
|
||||
MODE_PERMANENT_HOLD = 2
|
||||
MODE_TEMPORARY_HOLD = 1
|
||||
MODE_HOLD = {MODE_PERMANENT_HOLD, MODE_TEMPORARY_HOLD}
|
||||
|
||||
ATTR_FAN_ACTION = "fan_action"
|
||||
|
||||
ATTR_PERMANENT_HOLD = "permanent_hold"
|
||||
@@ -175,6 +179,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
self._cool_away_temp = cool_away_temp
|
||||
self._heat_away_temp = heat_away_temp
|
||||
self._away = False
|
||||
self._away_hold = False
|
||||
self._retry = 0
|
||||
|
||||
self._attr_unique_id = str(device.deviceid)
|
||||
@@ -323,11 +328,15 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
if self._away:
|
||||
if self._away and self._is_hold():
|
||||
self._away_hold = True
|
||||
return PRESET_AWAY
|
||||
if self._is_permanent_hold():
|
||||
if self._is_hold():
|
||||
return PRESET_HOLD
|
||||
|
||||
# Someone has changed the stat manually out of hold in away mode
|
||||
if self._away and self._away_hold:
|
||||
self._away = False
|
||||
self._away_hold = False
|
||||
return PRESET_NONE
|
||||
|
||||
@property
|
||||
@@ -335,10 +344,15 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
"""Return the fan setting."""
|
||||
return HW_FAN_MODE_TO_HA.get(self._device.fan_mode)
|
||||
|
||||
def _is_hold(self) -> bool:
|
||||
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
|
||||
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
|
||||
return heat_status in MODE_HOLD or cool_status in MODE_HOLD
|
||||
|
||||
def _is_permanent_hold(self) -> bool:
|
||||
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
|
||||
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
|
||||
return heat_status == 2 or cool_status == 2
|
||||
return MODE_PERMANENT_HOLD in (heat_status, cool_status)
|
||||
|
||||
async def _set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
|
||||
@@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HAP_SUFFIX = "._hap._tcp.local."
|
||||
POWERVIEW_G2_SUFFIX = "._powerview._tcp.local."
|
||||
POWERVIEW_G3_SUFFIX = "._powerview-g3._tcp.local."
|
||||
POWERVIEW_G3_SUFFIX = "._PowerView-G3._tcp.local."
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, hub_address: str) -> dict[str, str]:
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiopvapi"],
|
||||
"requirements": ["aiopvapi==3.1.1"],
|
||||
"zeroconf": ["_powerview._tcp.local.", "_powerview-g3._tcp.local."]
|
||||
"zeroconf": ["_powerview._tcp.local.", "_PowerView-G3._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -125,7 +125,9 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, mower_id)},
|
||||
manufacturer="Husqvarna",
|
||||
model=self.mower_attributes.system.model,
|
||||
model=self.mower_attributes.system.model.removeprefix(
|
||||
"HUSQVARNA "
|
||||
).removeprefix("Husqvarna "),
|
||||
name=self.mower_attributes.system.name,
|
||||
serial_number=self.mower_attributes.system.serial_number,
|
||||
suggested_area="Garden",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"requirements": ["aioautomower==2024.9.3"]
|
||||
"requirements": ["aioautomower==2024.10.0"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ DEFAULT_WATERING_TIME = timedelta(minutes=15)
|
||||
|
||||
MANUFACTURER = "Hydrawise"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["imgw_pib==1.0.5"]
|
||||
"requirements": ["imgw_pib==1.0.6"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["deepmerge", "pyipp"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyipp==0.16.0"],
|
||||
"requirements": ["pyipp==0.17.0"],
|
||||
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pyipp import Marker, Printer
|
||||
@@ -19,7 +19,6 @@ from homeassistant.const import ATTR_LOCATION, PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import IPPConfigEntry
|
||||
from .const import (
|
||||
@@ -80,7 +79,7 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda printer: (utcnow() - timedelta(seconds=printer.info.uptime)),
|
||||
value_fn=lambda printer: printer.booted_at,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==3.2.0",
|
||||
"xknxproject==3.8.0",
|
||||
"xknxproject==3.8.1",
|
||||
"knx-frontend==2024.9.10.221729"
|
||||
],
|
||||
"single_config_entry": true
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/linkplay",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-linkplay==0.0.12"],
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.0.15"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/matrix",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"requirements": ["matrix-nio==0.25.1", "Pillow==10.4.0"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==10.4.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.9.2"]
|
||||
"requirements": ["aiomealie==0.9.3"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp==2024.09.27"],
|
||||
"requirements": ["yt-dlp==2024.10.22"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"wrong_account": "You can only reauthenticate this entry with the same microBees account."
|
||||
},
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "assumed_state",
|
||||
"loggers": ["motionblindsble"],
|
||||
"requirements": ["motionblindsble==0.1.1"]
|
||||
"requirements": ["motionblindsble==0.1.2"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from nyt_games import NYTGamesClient
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .coordinator import NYTGamesCoordinator
|
||||
|
||||
@@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NYTGamesConfigEntry) ->
|
||||
"""Set up NYTGames from a config entry."""
|
||||
|
||||
client = NYTGamesClient(
|
||||
entry.data[CONF_TOKEN], session=async_get_clientsession(hass)
|
||||
entry.data[CONF_TOKEN], session=async_create_clientsession(hass)
|
||||
)
|
||||
|
||||
coordinator = NYTGamesCoordinator(hass, client)
|
||||
|
||||
@@ -7,7 +7,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
@@ -21,8 +21,9 @@ class NYTGamesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = NYTGamesClient(user_input[CONF_TOKEN], session=session)
|
||||
session = async_create_clientsession(self.hass)
|
||||
token = user_input[CONF_TOKEN].strip()
|
||||
client = NYTGamesClient(token, session=session)
|
||||
try:
|
||||
user_id = await client.get_user_id()
|
||||
except NYTGamesAuthenticationError:
|
||||
@@ -35,7 +36,9 @@ class NYTGamesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
await self.async_set_unique_id(str(user_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title="NYT Games", data=user_input)
|
||||
return self.async_create_entry(
|
||||
title="NYT Games", data={CONF_TOKEN: token}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_TOKEN): str}),
|
||||
|
||||
@@ -23,7 +23,7 @@ class NYTGamesData:
|
||||
|
||||
wordle: Wordle
|
||||
spelling_bee: SpellingBee | None
|
||||
connections: Connections
|
||||
connections: Connections | None
|
||||
|
||||
|
||||
class NYTGamesCoordinator(DataUpdateCoordinator[NYTGamesData]):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nyt_games",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["nyt_games==0.4.2"]
|
||||
"requirements": ["nyt_games==0.4.4"]
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ CONNECTIONS_SENSORS: tuple[NYTGamesConnectionsSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
value_fn=lambda connections: connections.current_streak,
|
||||
value_fn=lambda connections: connections.max_streak,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -161,10 +161,11 @@ async def async_setup_entry(
|
||||
NYTGamesSpellingBeeSensor(coordinator, description)
|
||||
for description in SPELLING_BEE_SENSORS
|
||||
)
|
||||
entities.extend(
|
||||
NYTGamesConnectionsSensor(coordinator, description)
|
||||
for description in CONNECTIONS_SENSORS
|
||||
)
|
||||
if coordinator.data.connections is not None:
|
||||
entities.extend(
|
||||
NYTGamesConnectionsSensor(coordinator, description)
|
||||
for description in CONNECTIONS_SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -236,4 +237,5 @@ class NYTGamesConnectionsSensor(ConnectionsEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType | date:
|
||||
"""Return the state of the sensor."""
|
||||
assert self.coordinator.data.connections is not None
|
||||
return self.entity_description.value_fn(self.coordinator.data.connections)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyotgw"],
|
||||
"requirements": ["pyotgw==2.2.1"]
|
||||
"requirements": ["pyotgw==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -130,20 +130,32 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
continue
|
||||
start = cost_reads[0].start_time
|
||||
_LOGGER.debug("Getting statistics at: %s", start)
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
statistics_during_period,
|
||||
self.hass,
|
||||
start,
|
||||
start + timedelta(seconds=1),
|
||||
{cost_statistic_id, consumption_statistic_id},
|
||||
"hour",
|
||||
None,
|
||||
{"sum"},
|
||||
)
|
||||
# In the common case there should be a previous statistic at start time
|
||||
# so we only need to fetch one statistic. If there isn't any, fetch all.
|
||||
for end in (start + timedelta(seconds=1), None):
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
statistics_during_period,
|
||||
self.hass,
|
||||
start,
|
||||
end,
|
||||
{cost_statistic_id, consumption_statistic_id},
|
||||
"hour",
|
||||
None,
|
||||
{"sum"},
|
||||
)
|
||||
if stats:
|
||||
break
|
||||
if end:
|
||||
_LOGGER.debug(
|
||||
"Not found. Trying to find the oldest statistic after %s",
|
||||
start,
|
||||
)
|
||||
# We are in this code path only if get_last_statistics found a stat
|
||||
# so statistics_during_period should also have found at least one.
|
||||
assert stats
|
||||
cost_sum = cast(float, stats[cost_statistic_id][0]["sum"])
|
||||
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
|
||||
last_stats_time = stats[consumption_statistic_id][0]["start"]
|
||||
assert last_stats_time == start.timestamp()
|
||||
|
||||
cost_statistics = []
|
||||
consumption_statistics = []
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.8.0"]
|
||||
"requirements": ["opower==0.8.3"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
"already_configured": "The Thread border router is already configured",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
client_session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
if custom_account := entry.data.get(CONF_ACCOUNT) is not None:
|
||||
if (custom_account := entry.data.get(CONF_ACCOUNT)) is not None:
|
||||
client.custom_account_id = custom_account
|
||||
|
||||
try:
|
||||
@@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_update_data() -> OVODailyUsage:
|
||||
"""Fetch data from OVO Energy."""
|
||||
if custom_account := entry.data.get(CONF_ACCOUNT) is not None:
|
||||
if (custom_account := entry.data.get(CONF_ACCOUNT)) is not None:
|
||||
client.custom_account_id = custom_account
|
||||
|
||||
async with asyncio.timeout(10):
|
||||
|
||||
@@ -46,7 +46,7 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
|
||||
if custom_account := user_input.get(CONF_ACCOUNT) is not None:
|
||||
if (custom_account := user_input.get(CONF_ACCOUNT)) is not None:
|
||||
client.custom_account_id = custom_account
|
||||
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,7 @@ from aiopyarr import exceptions
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
from aiopyarr.radarr_client import RadarrClient
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
@@ -54,6 +55,12 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
user_input = dict(self.entry.data) if self.entry else None
|
||||
|
||||
else:
|
||||
# aiopyarr defaults to the service port if one isn't given
|
||||
# this is counter to standard practice where http = 80
|
||||
# and https = 443.
|
||||
url = URL(user_input[CONF_URL])
|
||||
user_input[CONF_URL] = f"{url.scheme}://{url.host}:{url.port}{url.path}"
|
||||
|
||||
try:
|
||||
if result := await validate_input(self.hass, user_input):
|
||||
user_input[CONF_API_KEY] = result[1]
|
||||
|
||||
@@ -10,13 +10,9 @@ import uuid
|
||||
from ring_doorbell import Auth, Ring, RingDevices
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import APPLICATION_NAME, CONF_TOKEN
|
||||
from homeassistant.const import APPLICATION_NAME, CONF_DEVICE_ID, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
instance_id,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_LISTEN_CREDENTIALS, DOMAIN, PLATFORMS
|
||||
@@ -38,18 +34,12 @@ class RingData:
|
||||
type RingConfigEntry = ConfigEntry[RingData]
|
||||
|
||||
|
||||
async def get_auth_agent_id(hass: HomeAssistant) -> tuple[str, str]:
|
||||
"""Return user-agent and hardware id for Auth instantiation.
|
||||
def get_auth_user_agent() -> str:
|
||||
"""Return user-agent for Auth instantiation.
|
||||
|
||||
user_agent will be the display name in the ring.com authorised devices.
|
||||
hardware_id will uniquely describe the authorised HA device.
|
||||
"""
|
||||
user_agent = f"{APPLICATION_NAME}/{DOMAIN}-integration"
|
||||
|
||||
# Generate a new uuid from the instance_uuid to keep the HA one private
|
||||
instance_uuid = uuid.UUID(hex=await instance_id.async_get(hass))
|
||||
hardware_id = str(uuid.uuid5(instance_uuid, user_agent))
|
||||
return user_agent, hardware_id
|
||||
return f"{APPLICATION_NAME}/{DOMAIN}-integration"
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bool:
|
||||
@@ -69,13 +59,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bool
|
||||
data={**entry.data, CONF_LISTEN_CREDENTIALS: token},
|
||||
)
|
||||
|
||||
user_agent, hardware_id = await get_auth_agent_id(hass)
|
||||
user_agent = get_auth_user_agent()
|
||||
client_session = async_get_clientsession(hass)
|
||||
auth = Auth(
|
||||
user_agent,
|
||||
entry.data[CONF_TOKEN],
|
||||
token_updater,
|
||||
hardware_id=hardware_id,
|
||||
hardware_id=entry.data[CONF_DEVICE_ID],
|
||||
http_client_session=client_session,
|
||||
)
|
||||
ring = Ring(auth)
|
||||
@@ -138,3 +128,25 @@ async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None:
|
||||
return None
|
||||
|
||||
await er.async_migrate_entries(hass, entry_id, _async_migrator)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate old config entry."""
|
||||
entry_version = entry.version
|
||||
entry_minor_version = entry.minor_version
|
||||
|
||||
new_minor_version = 2
|
||||
if entry_version == 1 and entry_minor_version == 1:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry_version, entry_minor_version
|
||||
)
|
||||
hardware_id = str(uuid.uuid4())
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={**entry.data, CONF_DEVICE_ID: hardware_id},
|
||||
minor_version=new_minor_version,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -3,18 +3,25 @@
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from ring_doorbell import Auth, AuthenticationError, Requires2FAError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_TOKEN,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from . import get_auth_agent_id
|
||||
from .const import CONF_2FA, DOMAIN
|
||||
from . import get_auth_user_agent
|
||||
from .const import CONF_2FA, CONF_CONFIG_ENTRY_MINOR_VERSION, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,11 +30,15 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
||||
|
||||
STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]:
|
||||
|
||||
async def validate_input(
|
||||
hass: HomeAssistant, hardware_id: str, data: dict[str, str]
|
||||
) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
user_agent, hardware_id = await get_auth_agent_id(hass)
|
||||
user_agent = get_auth_user_agent()
|
||||
auth = Auth(
|
||||
user_agent,
|
||||
http_client_session=async_get_clientsession(hass),
|
||||
@@ -52,8 +63,10 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Ring."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION
|
||||
|
||||
user_pass: dict[str, Any] = {}
|
||||
hardware_id: str | None = None
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(
|
||||
@@ -64,8 +77,10 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||
self._abort_if_unique_id_configured()
|
||||
if not self.hardware_id:
|
||||
self.hardware_id = str(uuid.uuid4())
|
||||
try:
|
||||
token = await validate_input(self.hass, user_input)
|
||||
token = await validate_input(self.hass, self.hardware_id, user_input)
|
||||
except Require2FA:
|
||||
self.user_pass = user_input
|
||||
|
||||
@@ -78,7 +93,11 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data={CONF_USERNAME: user_input[CONF_USERNAME], CONF_TOKEN: token},
|
||||
data={
|
||||
CONF_DEVICE_ID: self.hardware_id,
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_TOKEN: token,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -120,8 +139,13 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input:
|
||||
user_input[CONF_USERNAME] = self.reauth_entry.data[CONF_USERNAME]
|
||||
# Reauth will use the same hardware id and re-authorise an existing
|
||||
# authorised device.
|
||||
if not self.hardware_id:
|
||||
self.hardware_id = self.reauth_entry.data[CONF_DEVICE_ID]
|
||||
assert self.hardware_id
|
||||
try:
|
||||
token = await validate_input(self.hass, user_input)
|
||||
token = await validate_input(self.hass, self.hardware_id, user_input)
|
||||
except Require2FA:
|
||||
self.user_pass = user_input
|
||||
return await self.async_step_2fa()
|
||||
@@ -134,6 +158,7 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data = {
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_TOKEN: token,
|
||||
CONF_DEVICE_ID: self.hardware_id,
|
||||
}
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.reauth_entry, data=data
|
||||
@@ -146,7 +171,8 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
CONF_USERNAME: self.reauth_entry.data[CONF_USERNAME]
|
||||
CONF_USERNAME: self.reauth_entry.data[CONF_USERNAME],
|
||||
CONF_NAME: self.reauth_entry.data[CONF_USERNAME],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
@@ -31,3 +32,5 @@ SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
CONF_2FA = "2fa"
|
||||
CONF_LISTEN_CREDENTIALS = "listen_token"
|
||||
|
||||
CONF_CONFIG_ENTRY_MINOR_VERSION: Final = 2
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ring-doorbell==0.9.6"]
|
||||
"requirements": ["ring-doorbell==0.9.8"]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import ACCOUNT_HASH, DOMAIN
|
||||
from .const import ACCOUNT_HASH, DOMAIN, UPDATE_INTERVAL
|
||||
from .coordinator import RitualsDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -37,9 +37,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Migrate old unique_ids to the new format
|
||||
async_migrate_entities_unique_ids(hass, entry, account_devices)
|
||||
|
||||
# The API provided by Rituals is currently rate limited to 30 requests
|
||||
# per hour per IP address. To avoid hitting this limit, we will adjust
|
||||
# the polling interval based on the number of diffusers one has.
|
||||
update_interval = UPDATE_INTERVAL * len(account_devices)
|
||||
|
||||
# Create a coordinator for each diffuser
|
||||
coordinators = {
|
||||
diffuser.hublot: RitualsDataUpdateCoordinator(hass, diffuser)
|
||||
diffuser.hublot: RitualsDataUpdateCoordinator(hass, diffuser, update_interval)
|
||||
for diffuser in account_devices
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ class RitualsPerfumeGenieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await account.authenticate()
|
||||
except ClientResponseError:
|
||||
_LOGGER.exception("Unexpected response")
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -6,4 +6,8 @@ DOMAIN = "rituals_perfume_genie"
|
||||
|
||||
ACCOUNT_HASH = "account_hash"
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=2)
|
||||
# The API provided by Rituals is currently rate limited to 30 requests
|
||||
# per hour per IP address. To avoid hitting this limit, the polling
|
||||
# interval is set to 3 minutes. This also gives a little room for
|
||||
# Home Assistant restarts.
|
||||
UPDATE_INTERVAL = timedelta(minutes=3)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The Rituals Perfume Genie data update coordinator."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyrituals import Diffuser
|
||||
@@ -7,7 +8,7 @@ from pyrituals import Diffuser
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, UPDATE_INTERVAL
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,14 +16,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Class to manage fetching Rituals Perfume Genie device data from single endpoint."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, diffuser: Diffuser) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
diffuser: Diffuser,
|
||||
update_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize global Rituals Perfume Genie data updater."""
|
||||
self.diffuser = diffuser
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"{DOMAIN}-{diffuser.hublot}",
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
|
||||
from .coordinator import RokuDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -24,7 +24,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
device_id = entry.entry_id
|
||||
|
||||
coordinator = RokuDataUpdateCoordinator(
|
||||
hass, host=entry.data[CONF_HOST], device_id=device_id
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
device_id=device_id,
|
||||
play_media_app_id=entry.options.get(
|
||||
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
|
||||
),
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -32,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -40,3 +47,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload the config entry when it changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
@@ -10,12 +10,17 @@ from rokuecp import Roku, RokuError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ssdp, zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
|
||||
@@ -155,3 +160,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=self.discovery_info[CONF_NAME],
|
||||
data=self.discovery_info,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowWithConfigEntry:
|
||||
"""Create the options flow."""
|
||||
return RokuOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class RokuOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle Roku options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage Roku options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PLAY_MEDIA_APP_ID,
|
||||
default=self.options.get(
|
||||
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
|
||||
),
|
||||
): str,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -15,3 +15,9 @@ DEFAULT_PORT = 8060
|
||||
|
||||
# Services
|
||||
SERVICE_SEARCH = "search"
|
||||
|
||||
# Config
|
||||
CONF_PLAY_MEDIA_APP_ID = "play_media_app_id"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_PLAY_MEDIA_APP_ID = "15985"
|
||||
|
||||
@@ -29,15 +29,12 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
roku: Roku
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
host: str,
|
||||
device_id: str,
|
||||
self, hass: HomeAssistant, *, host: str, device_id: str, play_media_app_id: str
|
||||
) -> None:
|
||||
"""Initialize global Roku data updater."""
|
||||
self.device_id = device_id
|
||||
self.roku = Roku(host=host, session=async_get_clientsession(hass))
|
||||
self.play_media_app_id = play_media_app_id
|
||||
|
||||
self.full_update_interval = timedelta(minutes=15)
|
||||
self.last_full_update = None
|
||||
|
||||
@@ -445,17 +445,25 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
if attr in extra
|
||||
}
|
||||
|
||||
params = {"t": "a", **params}
|
||||
params = {"u": media_id, "t": "a", **params}
|
||||
|
||||
await self.coordinator.roku.play_on_roku(media_id, params)
|
||||
await self.coordinator.roku.launch(
|
||||
self.coordinator.play_media_app_id,
|
||||
params,
|
||||
)
|
||||
elif media_type in {MediaType.URL, MediaType.VIDEO}:
|
||||
params = {
|
||||
param: extra[attr]
|
||||
for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
|
||||
if attr in extra
|
||||
}
|
||||
params["u"] = media_id
|
||||
params["t"] = "v"
|
||||
|
||||
await self.coordinator.roku.play_on_roku(media_id, params)
|
||||
await self.coordinator.roku.launch(
|
||||
self.coordinator.play_media_app_id,
|
||||
params,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Media type %s is not supported", original_media_type)
|
||||
return
|
||||
|
||||
@@ -24,6 +24,18 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"play_media_app_id": "Play Media Roku Application ID"
|
||||
},
|
||||
"data_description": {
|
||||
"play_media_app_id": "The application ID to use when launching media playback. Must support the PlayOnRoku API."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"headphones_connected": {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -90,13 +90,21 @@ class SchlageDataUpdateCoordinator(DataUpdateCoordinator[SchlageData]):
|
||||
devices = dr.async_entries_for_config_entry(
|
||||
device_registry, self.config_entry.entry_id
|
||||
)
|
||||
previous_locks = {device.id for device in devices}
|
||||
previous_locks = set()
|
||||
previous_locks_by_lock_id = {}
|
||||
for device in devices:
|
||||
for domain, identifier in device.identifiers:
|
||||
if domain == DOMAIN:
|
||||
previous_locks.add(identifier)
|
||||
previous_locks_by_lock_id[identifier] = device
|
||||
continue
|
||||
current_locks = set(self.data.locks.keys())
|
||||
|
||||
if removed_locks := previous_locks - current_locks:
|
||||
LOGGER.debug("Removed locks: %s", ", ".join(removed_locks))
|
||||
for device_id in removed_locks:
|
||||
for lock_id in removed_locks:
|
||||
device_registry.async_update_device(
|
||||
device_id=device_id,
|
||||
device_id=previous_locks_by_lock_id[lock_id].id,
|
||||
remove_config_entry_id=self.config_entry.entry_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable
|
||||
from contextlib import suppress
|
||||
import datetime
|
||||
from functools import partial
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
@@ -39,6 +38,7 @@ from homeassistant.helpers.entity import entity_sources
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -686,7 +686,6 @@ def list_statistic_ids(
|
||||
@callback
|
||||
def _update_issues(
|
||||
report_issue: Callable[[str, str, dict[str, Any]], None],
|
||||
clear_issue: Callable[[str, str], None],
|
||||
sensor_states: list[State],
|
||||
metadatas: dict[str, tuple[int, StatisticMetaData]],
|
||||
) -> None:
|
||||
@@ -707,8 +706,6 @@ def _update_issues(
|
||||
entity_id,
|
||||
{"statistic_id": entity_id},
|
||||
)
|
||||
else:
|
||||
clear_issue("state_class_removed", entity_id)
|
||||
|
||||
metadata_unit = metadata[1]["unit_of_measurement"]
|
||||
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
|
||||
@@ -725,8 +722,6 @@ def _update_issues(
|
||||
"supported_unit": metadata_unit,
|
||||
},
|
||||
)
|
||||
else:
|
||||
clear_issue("units_changed", entity_id)
|
||||
elif numeric and state_unit not in converter.VALID_UNITS:
|
||||
# The state unit can't be converted to the unit in metadata
|
||||
valid_units = (unit or "<None>" for unit in converter.VALID_UNITS)
|
||||
@@ -741,8 +736,6 @@ def _update_issues(
|
||||
"supported_unit": valid_units_str,
|
||||
},
|
||||
)
|
||||
else:
|
||||
clear_issue("units_changed", entity_id)
|
||||
|
||||
|
||||
def update_statistics_issues(
|
||||
@@ -756,36 +749,50 @@ def update_statistics_issues(
|
||||
instance, session, statistic_source=RECORDER_DOMAIN
|
||||
)
|
||||
|
||||
@callback
|
||||
def get_sensor_statistics_issues(hass: HomeAssistant) -> set[str]:
|
||||
"""Return a list of statistics issues."""
|
||||
issues = set()
|
||||
issue_registry = ir.async_get(hass)
|
||||
for issue in issue_registry.issues.values():
|
||||
if (
|
||||
issue.domain != DOMAIN
|
||||
or not (issue_data := issue.data)
|
||||
or issue_data.get("issue_type")
|
||||
not in ("state_class_removed", "units_changed")
|
||||
):
|
||||
continue
|
||||
issues.add(issue.issue_id)
|
||||
return issues
|
||||
|
||||
issues = run_callback_threadsafe(
|
||||
hass.loop, get_sensor_statistics_issues, hass
|
||||
).result()
|
||||
|
||||
def create_issue_registry_issue(
|
||||
issue_type: str, statistic_id: str, data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Create an issue registry issue."""
|
||||
hass.loop.call_soon_threadsafe(
|
||||
partial(
|
||||
ir.async_create_issue,
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{issue_type}_{statistic_id}",
|
||||
data=data | {"issue_type": issue_type},
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=issue_type,
|
||||
translation_placeholders=data,
|
||||
)
|
||||
)
|
||||
|
||||
def delete_issue_registry_issue(issue_type: str, statistic_id: str) -> None:
|
||||
"""Delete an issue registry issue."""
|
||||
hass.loop.call_soon_threadsafe(
|
||||
ir.async_delete_issue, hass, DOMAIN, f"{issue_type}_{statistic_id}"
|
||||
issue_id = f"{issue_type}_{statistic_id}"
|
||||
issues.discard(issue_id)
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
data=data | {"issue_type": issue_type},
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=issue_type,
|
||||
translation_placeholders=data,
|
||||
)
|
||||
|
||||
_update_issues(
|
||||
create_issue_registry_issue,
|
||||
delete_issue_registry_issue,
|
||||
sensor_states,
|
||||
metadatas,
|
||||
)
|
||||
for issue_id in issues:
|
||||
hass.loop.call_soon_threadsafe(ir.async_delete_issue, hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
def validate_statistics(
|
||||
@@ -811,7 +818,6 @@ def validate_statistics(
|
||||
|
||||
_update_issues(
|
||||
create_statistic_validation_issue,
|
||||
lambda issue_type, statistic_id: None,
|
||||
sensor_states,
|
||||
metadatas,
|
||||
)
|
||||
|
||||
@@ -135,3 +135,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
data[PYSMA_REMOVE_LISTENER]()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
|
||||
_LOGGER.debug("Migrating from version %s", entry.version)
|
||||
|
||||
if entry.version == 1:
|
||||
# 1 -> 2: Unique ID from integer to string
|
||||
if entry.minor_version == 1:
|
||||
minor_version = 2
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, unique_id=str(entry.unique_id), minor_version=minor_version
|
||||
)
|
||||
|
||||
_LOGGER.debug("Migration successful")
|
||||
|
||||
return True
|
||||
|
||||
@@ -40,6 +40,7 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for SMA."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
@@ -76,7 +77,7 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(device_info["serial"])
|
||||
await self.async_set_unique_id(str(device_info["serial"]))
|
||||
self._abort_if_unique_id_configured(updates=self._data)
|
||||
return self.async_create_entry(
|
||||
title=self._data[CONF_HOST], data=self._data
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smlight",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pysmlight==0.1.1"],
|
||||
"requirements": ["pysmlight==0.1.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -38,7 +38,7 @@ class SolarLogCoordinatorEntity(SolarLogBaseEntity):
|
||||
"""Initialize the SolarLogCoordinator sensor."""
|
||||
super().__init__(coordinator, description)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
|
||||
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer="Solar-Log",
|
||||
model="Controller",
|
||||
@@ -59,8 +59,8 @@ class SolarLogInverterEntity(SolarLogBaseEntity):
|
||||
) -> None:
|
||||
"""Initialize the SolarLogInverter sensor."""
|
||||
super().__init__(coordinator, description)
|
||||
name = f"{coordinator.unique_id}-{slugify(coordinator.solarlog.device_name(device_id))}"
|
||||
self._attr_unique_id = f"{name}-{description.key}"
|
||||
name = f"{coordinator.unique_id}_{slugify(coordinator.solarlog.device_name(device_id))}"
|
||||
self._attr_unique_id = f"{name}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer="Solar-Log",
|
||||
model="Inverter",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/solarlog",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["solarlog_cli"],
|
||||
"requirements": ["solarlog_cli==0.3.0"]
|
||||
"requirements": ["solarlog_cli==0.3.2"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"reconfigure_confirm": {
|
||||
"title": "Configure SolarLog",
|
||||
"data": {
|
||||
"has_password": "[%key:component::solarlog::config::step::user::data::has_password%]"
|
||||
"has_password": "[%key:component::solarlog::config::step::user::data::has_password%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,12 +27,6 @@
|
||||
},
|
||||
"call_query": {
|
||||
"service": "mdi:database"
|
||||
},
|
||||
"sync": {
|
||||
"service": "mdi:sync"
|
||||
},
|
||||
"unsync": {
|
||||
"service": "mdi:sync-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,19 +30,3 @@ call_query:
|
||||
advanced: true
|
||||
selector:
|
||||
object:
|
||||
sync:
|
||||
target:
|
||||
entity:
|
||||
integration: squeezebox
|
||||
domain: media_player
|
||||
fields:
|
||||
other_player:
|
||||
required: true
|
||||
example: "media_player.living_room"
|
||||
selector:
|
||||
text:
|
||||
unsync:
|
||||
target:
|
||||
entity:
|
||||
integration: squeezebox
|
||||
domain: media_player
|
||||
|
||||
@@ -60,20 +60,6 @@
|
||||
"description": "[%key:component::squeezebox::services::call_method::fields::parameters::description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sync": {
|
||||
"name": "Sync",
|
||||
"description": "Adds another player to this player's sync group. If the other player is already in a sync group, it will leave it.\n.",
|
||||
"fields": {
|
||||
"other_player": {
|
||||
"name": "Other player",
|
||||
"description": "Name of the other Squeezebox player to link."
|
||||
}
|
||||
}
|
||||
},
|
||||
"unsync": {
|
||||
"name": "Unsync",
|
||||
"description": "Removes this player from its sync group."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user