mirror of
https://github.com/home-assistant/core.git
synced 2026-01-13 11:07:59 +01:00
Compare commits
2 Commits
tibber_bin
...
scop-senso
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af18a4464 | ||
|
|
d101147394 |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.2"
|
||||
HA_SHORT_VERSION: "2026.1"
|
||||
DEFAULT_PYTHON: "3.13.11"
|
||||
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
7
CODEOWNERS
generated
7
CODEOWNERS
generated
@@ -516,8 +516,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/fireservicerota/ @cyberjunky
|
||||
/homeassistant/components/firmata/ @DaAwesomeP
|
||||
/tests/components/firmata/ @DaAwesomeP
|
||||
/homeassistant/components/fish_audio/ @noambav
|
||||
/tests/components/fish_audio/ @noambav
|
||||
/homeassistant/components/fitbit/ @allenporter
|
||||
/tests/components/fitbit/ @allenporter
|
||||
/homeassistant/components/fivem/ @Sander0542
|
||||
@@ -661,8 +659,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||
/tests/components/hassio/ @home-assistant/supervisor
|
||||
/homeassistant/components/hdfury/ @glenndehaan
|
||||
/tests/components/hdfury/ @glenndehaan
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
/tests/components/hdmi_cec/ @inytar
|
||||
/homeassistant/components/heatmiser/ @andylockran
|
||||
@@ -1172,8 +1168,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/open_router/ @joostlek
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openevse/ @c00w @firstof9
|
||||
/tests/components/openevse/ @c00w @firstof9
|
||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||
/tests/components/openexchangerates/ @MartinHjelmare
|
||||
/homeassistant/components/opengarage/ @danielhiversen
|
||||
@@ -1805,7 +1799,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/waqi/ @joostlek
|
||||
/homeassistant/components/water_heater/ @home-assistant/core
|
||||
/tests/components/water_heater/ @home-assistant/core
|
||||
/homeassistant/components/waterfurnace/ @sdague @masterkoppa
|
||||
/homeassistant/components/watergate/ @adam-the-hero
|
||||
/tests/components/watergate/ @adam-the-hero
|
||||
/homeassistant/components/watson_tts/ @rutkai
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["accuweather"],
|
||||
"requirements": ["accuweather==5.0.0"]
|
||||
"requirements": ["accuweather==4.2.2"]
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
||||
from .entity import ActronAirAcEntity, ActronAirZoneEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -54,7 +56,8 @@ async def async_setup_entry(
|
||||
|
||||
for coordinator in system_coordinators.values():
|
||||
status = coordinator.data
|
||||
entities.append(ActronSystemClimate(coordinator))
|
||||
name = status.ac_system.system_name
|
||||
entities.append(ActronSystemClimate(coordinator, name))
|
||||
|
||||
entities.extend(
|
||||
ActronZoneClimate(coordinator, zone)
|
||||
@@ -65,9 +68,10 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ActronAirClimateEntity(ClimateEntity):
|
||||
class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity):
|
||||
"""Base class for Actron Air climate entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
@@ -79,17 +83,43 @@ class ActronAirClimateEntity(ClimateEntity):
|
||||
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
|
||||
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ActronAirSystemCoordinator,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize an Actron Air unit."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_number = coordinator.serial_number
|
||||
|
||||
class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
|
||||
class ActronSystemClimate(BaseClimateEntity):
|
||||
"""Representation of the Actron Air system."""
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ActronAirSystemCoordinator,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize an Actron Air unit."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = self._serial_number
|
||||
super().__init__(coordinator, name)
|
||||
serial_number = coordinator.serial_number
|
||||
self._attr_unique_id = serial_number
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=self._status.ac_system.system_name,
|
||||
manufacturer="Actron Air",
|
||||
model_id=self._status.ac_system.master_wc_model,
|
||||
sw_version=self._status.ac_system.master_wc_firmware_version,
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
@@ -138,7 +168,7 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set a new fan mode."""
|
||||
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
|
||||
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower())
|
||||
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
@@ -152,7 +182,7 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
await self._status.user_aircon_settings.set_temperature(temperature=temp)
|
||||
|
||||
|
||||
class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
class ActronZoneClimate(BaseClimateEntity):
|
||||
"""Representation of a zone within the Actron Air system."""
|
||||
|
||||
_attr_supported_features = (
|
||||
@@ -167,8 +197,18 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
zone: ActronAirZone,
|
||||
) -> None:
|
||||
"""Initialize an Actron Air unit."""
|
||||
super().__init__(coordinator, zone)
|
||||
self._attr_unique_id: str = self._zone_identifier
|
||||
super().__init__(coordinator, zone.title)
|
||||
serial_number = coordinator.serial_number
|
||||
self._zone_id: int = zone.zone_id
|
||||
self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}"
|
||||
self._attr_device_info: DeviceInfo = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
name=zone.title,
|
||||
manufacturer="Actron Air",
|
||||
model="Zone",
|
||||
suggested_area=zone.title,
|
||||
via_device=(DOMAIN, serial_number),
|
||||
)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
@@ -216,4 +256,4 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the temperature."""
|
||||
await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))
|
||||
await self._zone.set_temperature(temperature=kwargs["temperature"])
|
||||
|
||||
@@ -8,7 +8,6 @@ from datetime import timedelta
|
||||
from actron_neo_api import (
|
||||
ActronAirACSystem,
|
||||
ActronAirAPI,
|
||||
ActronAirAPIError,
|
||||
ActronAirAuthError,
|
||||
ActronAirStatus,
|
||||
)
|
||||
@@ -16,7 +15,7 @@ from actron_neo_api import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
@@ -71,12 +70,6 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except ActronAirAPIError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||
self.last_seen = dt_util.utcnow()
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Base entity classes for Actron Air integration."""
|
||||
|
||||
from actron_neo_api import ActronAirZone
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ActronAirSystemCoordinator
|
||||
|
||||
|
||||
class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]):
|
||||
"""Base class for Actron Air entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: ActronAirSystemCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_number = coordinator.serial_number
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return not self.coordinator.is_device_stale()
|
||||
|
||||
|
||||
class ActronAirAcEntity(ActronAirEntity):
|
||||
"""Base class for Actron Air entities."""
|
||||
|
||||
def __init__(self, coordinator: ActronAirSystemCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._serial_number)},
|
||||
name=coordinator.data.ac_system.system_name,
|
||||
manufacturer="Actron Air",
|
||||
model_id=coordinator.data.ac_system.master_wc_model,
|
||||
sw_version=coordinator.data.ac_system.master_wc_firmware_version,
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
|
||||
|
||||
class ActronAirZoneEntity(ActronAirEntity):
|
||||
"""Base class for Actron Air zone entities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ActronAirSystemCoordinator,
|
||||
zone: ActronAirZone,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._zone_id: int = zone.zone_id
|
||||
self._zone_identifier = f"{self._serial_number}_zone_{zone.zone_id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._zone_identifier)},
|
||||
name=zone.title,
|
||||
manufacturer="Actron Air",
|
||||
model="Zone",
|
||||
suggested_area=zone.title,
|
||||
via_device=(DOMAIN, self._serial_number),
|
||||
)
|
||||
@@ -51,9 +51,6 @@
|
||||
"exceptions": {
|
||||
"auth_error": {
|
||||
"message": "Authentication failed, please reauthenticate"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the Actron Air API: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ from typing import Any
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
||||
from .entity import ActronAirAcEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -72,9 +74,10 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class ActronAirSwitch(ActronAirAcEntity, SwitchEntity):
|
||||
class ActronAirSwitch(CoordinatorEntity[ActronAirSystemCoordinator], SwitchEntity):
|
||||
"""Actron Air switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
entity_description: ActronAirSwitchEntityDescription
|
||||
|
||||
@@ -87,6 +90,11 @@ class ActronAirSwitch(ActronAirAcEntity, SwitchEntity):
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.serial_number)},
|
||||
manufacturer="Actron Air",
|
||||
name=coordinator.data.ac_system.system_name,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["adax", "adax_local"],
|
||||
"requirements": ["adax==0.4.0", "Adax-local==0.3.0"]
|
||||
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -7,12 +7,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Button platform for Airobot integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pyairobotrest.exceptions import (
|
||||
AirobotConnectionError,
|
||||
AirobotError,
|
||||
AirobotTimeoutError,
|
||||
)
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
||||
from .entity import AirobotEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirobotButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Airobot button entity."""
|
||||
|
||||
press_fn: Callable[[AirobotDataUpdateCoordinator], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
BUTTON_TYPES: tuple[AirobotButtonEntityDescription, ...] = (
|
||||
AirobotButtonEntityDescription(
|
||||
key="restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda coordinator: coordinator.client.reboot_thermostat(),
|
||||
),
|
||||
AirobotButtonEntityDescription(
|
||||
key="recalibrate_co2",
|
||||
translation_key="recalibrate_co2",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
press_fn=lambda coordinator: coordinator.client.recalibrate_co2_sensor(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AirobotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Airobot button entities."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AirobotButton(coordinator, description) for description in BUTTON_TYPES
|
||||
)
|
||||
|
||||
|
||||
class AirobotButton(AirobotEntity, ButtonEntity):
|
||||
"""Representation of an Airobot button."""
|
||||
|
||||
entity_description: AirobotButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirobotDataUpdateCoordinator,
|
||||
description: AirobotButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator)
|
||||
except (AirobotConnectionError, AirobotTimeoutError):
|
||||
# Connection errors during reboot are expected as device restarts
|
||||
pass
|
||||
except AirobotError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="button_press_failed",
|
||||
translation_placeholders={"button": self.entity_description.key},
|
||||
) from err
|
||||
@@ -175,42 +175,6 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the integration."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
# Verify the device ID matches the existing config entry
|
||||
await self.async_set_unique_id(info.device_id)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_device")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates=user_input,
|
||||
title=info.title,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, reconfigure_entry.data
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"recalibrate_co2": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"hysteresis_band": {
|
||||
"default": "mdi:delta"
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyairobotrest==0.2.0"]
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyairobotrest==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
@@ -58,7 +58,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: This integration doesn't have any cases where raising an issue is needed.
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_device": "Device ID does not match the existing configuration. Please use the correct device credentials."
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -30,19 +28,6 @@
|
||||
},
|
||||
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "Device ID"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::airobot::config::step::user::data_description::host%]",
|
||||
"password": "[%key:component::airobot::config::step::user::data_description::password%]",
|
||||
"username": "[%key:component::airobot::config::step::user::data_description::username%]"
|
||||
},
|
||||
"description": "Update your Airobot thermostat connection details. Note: The Device ID must remain the same as the original configuration."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
@@ -59,11 +44,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"recalibrate_co2": {
|
||||
"name": "Recalibrate CO2 sensor"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"hysteresis_band": {
|
||||
"name": "Hysteresis band"
|
||||
@@ -91,9 +71,6 @@
|
||||
"authentication_failed": {
|
||||
"message": "Authentication failed, please reauthenticate."
|
||||
},
|
||||
"button_press_failed": {
|
||||
"message": "Failed to press {button} button."
|
||||
},
|
||||
"connection_failed": {
|
||||
"message": "Failed to communicate with device."
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["airos==0.6.1"]
|
||||
"requirements": ["airos==0.6.0"]
|
||||
}
|
||||
|
||||
@@ -85,22 +85,6 @@ class AirzoneSystemEntity(AirzoneEntity):
|
||||
value = system[key]
|
||||
return value
|
||||
|
||||
async def _async_update_sys_params(self, params: dict[str, Any]) -> None:
|
||||
"""Send system parameters to API."""
|
||||
_params = {
|
||||
API_SYSTEM_ID: self.system_id,
|
||||
**params,
|
||||
}
|
||||
_LOGGER.debug("update_sys_params=%s", _params)
|
||||
try:
|
||||
await self.coordinator.airzone.set_sys_parameters(_params)
|
||||
except AirzoneError as error:
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set system {self.entity_id}: {error}"
|
||||
) from error
|
||||
|
||||
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
|
||||
|
||||
|
||||
class AirzoneHotWaterEntity(AirzoneEntity):
|
||||
"""Define an Airzone Hot Water entity."""
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.5"]
|
||||
"requirements": ["aioairzone==1.0.4"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ from aioairzone.const import (
|
||||
AZD_MODES,
|
||||
AZD_Q_ADAPT,
|
||||
AZD_SLEEP,
|
||||
AZD_SYSTEMS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
@@ -31,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -86,18 +85,6 @@ def main_zone_options(
|
||||
return [k for k, v in options.items() if v in modes]
|
||||
|
||||
|
||||
SYSTEM_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_Q_ADAPT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
key=AZD_Q_ADAPT,
|
||||
options=list(Q_ADAPT_DICT),
|
||||
options_dict=Q_ADAPT_DICT,
|
||||
translation_key="q_adapt",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_MODE,
|
||||
@@ -106,6 +93,14 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_Q_ADAPT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
key=AZD_Q_ADAPT,
|
||||
options=list(Q_ADAPT_DICT),
|
||||
options_dict=Q_ADAPT_DICT,
|
||||
translation_key="q_adapt",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -145,37 +140,16 @@ async def async_setup_entry(
|
||||
"""Add Airzone select from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
added_systems: set[str] = set()
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of select."""
|
||||
|
||||
entities: list[AirzoneBaseSelect] = []
|
||||
|
||||
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
|
||||
received_systems = set(systems_data)
|
||||
new_systems = received_systems - added_systems
|
||||
if new_systems:
|
||||
entities.extend(
|
||||
AirzoneSystemSelect(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_id,
|
||||
systems_data.get(system_id),
|
||||
)
|
||||
for system_id in new_systems
|
||||
for description in SYSTEM_SELECT_TYPES
|
||||
if description.key in systems_data.get(system_id)
|
||||
)
|
||||
added_systems.update(new_systems)
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
entities.extend(
|
||||
entities: list[AirzoneZoneSelect] = [
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -187,8 +161,8 @@ async def async_setup_entry(
|
||||
for description in MAIN_ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
|
||||
)
|
||||
entities.extend(
|
||||
]
|
||||
entities += [
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -199,11 +173,10 @@ async def async_setup_entry(
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
)
|
||||
]
|
||||
async_add_entities(entities)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
@@ -230,38 +203,6 @@ class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||
self._attr_current_option = self._get_current_option()
|
||||
|
||||
|
||||
class AirzoneSystemSelect(AirzoneSystemEntity, AirzoneBaseSelect):
|
||||
"""Define an Airzone System select."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneSelectDescription,
|
||||
entry: ConfigEntry,
|
||||
system_id: str,
|
||||
system_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, system_data)
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_options = self.entity_description.options_fn(
|
||||
system_data, description.options_dict
|
||||
)
|
||||
|
||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
param = self.entity_description.api_param
|
||||
value = self.entity_description.options_dict[option]
|
||||
await self._async_update_sys_params({param: value})
|
||||
|
||||
|
||||
class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
||||
"""Define an Airzone Zone select."""
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==11.0.2"]
|
||||
"requirements": ["aioamazondevices==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, llm
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
@@ -194,7 +193,7 @@ def _convert_content(
|
||||
tool_result_block = ToolResultBlockParam(
|
||||
type="tool_result",
|
||||
tool_use_id=content.tool_call_id,
|
||||
content=json_dumps(content.tool_result),
|
||||
content=json.dumps(content.tool_result),
|
||||
)
|
||||
external_tool = False
|
||||
if not messages or messages[-1]["role"] != (
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import dateutil
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -181,7 +179,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
LAST_S_TEST: SensorEntityDescription(
|
||||
key=LAST_S_TEST,
|
||||
translation_key="last_self_test",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
"lastxfer": SensorEntityDescription(
|
||||
key="lastxfer",
|
||||
@@ -235,7 +232,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"masterupd": SensorEntityDescription(
|
||||
key="masterupd",
|
||||
translation_key="master_update",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"maxlinev": SensorEntityDescription(
|
||||
@@ -369,7 +365,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"starttime": SensorEntityDescription(
|
||||
key="starttime",
|
||||
translation_key="startup_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"statflag": SensorEntityDescription(
|
||||
@@ -421,19 +416,16 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"xoffbat": SensorEntityDescription(
|
||||
key="xoffbat",
|
||||
translation_key="transfer_from_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xoffbatt": SensorEntityDescription(
|
||||
key="xoffbatt",
|
||||
translation_key="transfer_from_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xonbatt": SensorEntityDescription(
|
||||
key="xonbatt",
|
||||
translation_key="transfer_to_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
@@ -537,13 +529,7 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
data = self.coordinator.data[key]
|
||||
|
||||
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
return
|
||||
|
||||
self._attr_native_value, inferred_unit = infer_unit(data)
|
||||
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
||||
if not self.native_unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = inferred_unit
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["pysilero-vad==3.2.0", "pyspeex-noise==1.0.2"]
|
||||
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import asyncio
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Literal, Protocol, cast
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
@@ -16,10 +16,7 @@ from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
|
||||
from homeassistant.components.labs import async_listen as async_labs_listen
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FLOOR_ID,
|
||||
ATTR_LABEL_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ACTIONS,
|
||||
@@ -33,7 +30,6 @@ from homeassistant.const import (
|
||||
CONF_OPTIONS,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_TARGET,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
@@ -140,7 +136,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"scene",
|
||||
"siren",
|
||||
"switch",
|
||||
@@ -593,32 +588,20 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
"""Return True if entity is on."""
|
||||
return self._async_detach_triggers is not None or self._is_enabled
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def referenced_labels(self) -> set[str]:
|
||||
"""Return a set of referenced labels."""
|
||||
referenced = self.action_script.referenced_labels
|
||||
return self.action_script.referenced_labels
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
|
||||
return referenced
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def referenced_floors(self) -> set[str]:
|
||||
"""Return a set of referenced floors."""
|
||||
referenced = self.action_script.referenced_floors
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
|
||||
return referenced
|
||||
return self.action_script.referenced_floors
|
||||
|
||||
@cached_property
|
||||
def referenced_areas(self) -> set[str]:
|
||||
"""Return a set of referenced areas."""
|
||||
referenced = self.action_script.referenced_areas
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
|
||||
return referenced
|
||||
return self.action_script.referenced_areas
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
@@ -1226,9 +1209,6 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
|
||||
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
|
||||
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
|
||||
|
||||
if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
|
||||
return target_devices
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@@ -1259,28 +1239,9 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
|
||||
|
||||
if target_entities := _get_targets_from_trigger_config(
|
||||
trigger_conf, CONF_ENTITY_ID
|
||||
):
|
||||
return target_entities
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@callback
|
||||
def _get_targets_from_trigger_config(
|
||||
config: dict,
|
||||
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
|
||||
) -> list[str]:
|
||||
"""Extract targets from a target config."""
|
||||
if not (target_conf := config.get(CONF_TARGET)):
|
||||
return []
|
||||
if not (targets := target_conf.get(target)):
|
||||
return []
|
||||
|
||||
return [targets] if isinstance(targets, str) else targets
|
||||
|
||||
|
||||
@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
|
||||
def websocket_config(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -80,7 +80,7 @@ class AzureDataExplorerClient:
|
||||
def test_connection(self) -> None:
|
||||
"""Test connection, will throw Exception if it cannot connect."""
|
||||
|
||||
query = f"['{self._table}'] | take 1"
|
||||
query = f"{self._table} | take 1"
|
||||
|
||||
self.query_client.execute_query(self._database, query)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class ADXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def validate_input(self, data: dict[str, Any]) -> dict[str, str]:
|
||||
async def validate_input(self, data: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
@@ -54,40 +54,36 @@ class ADXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.test_connection)
|
||||
except KustoAuthenticationError as err:
|
||||
_LOGGER.error("Authentication failed: %s", err)
|
||||
|
||||
except KustoAuthenticationError as exp:
|
||||
_LOGGER.error(exp)
|
||||
return {"base": "invalid_auth"}
|
||||
except KustoServiceError as err:
|
||||
_LOGGER.error("Could not connect: %s", err)
|
||||
|
||||
except KustoServiceError as exp:
|
||||
_LOGGER.error(exp)
|
||||
return {"base": "cannot_connect"}
|
||||
|
||||
return {}
|
||||
return None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
data_schema = STEP_USER_DATA_SCHEMA
|
||||
|
||||
if user_input is not None:
|
||||
errors = await self.validate_input(user_input)
|
||||
errors: dict = {}
|
||||
if user_input:
|
||||
errors = await self.validate_input(user_input) # type: ignore[assignment]
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
data=user_input,
|
||||
title=f"{user_input[CONF_ADX_CLUSTER_INGEST_URI].replace('https://', '')} / {user_input[CONF_ADX_DATABASE_NAME]} ({user_input[CONF_ADX_TABLE_NAME]})",
|
||||
title=user_input[CONF_ADX_CLUSTER_INGEST_URI].replace(
|
||||
"https://", ""
|
||||
),
|
||||
options=DEFAULT_OPTIONS,
|
||||
)
|
||||
|
||||
# Keep previously entered values when we re-show the form after an error.
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"use_queued_ingestion": "Use queued ingestion"
|
||||
},
|
||||
"data_description": {
|
||||
"authority_id": "In Azure portal this is also known as Directory (tenant) ID",
|
||||
"cluster_ingest_uri": "Ingestion URI of the cluster",
|
||||
"use_queued_ingestion": "Must be enabled when using ADX free cluster"
|
||||
},
|
||||
|
||||
@@ -6,15 +6,13 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from b2sdk.v2 import Bucket, exception
|
||||
from b2sdk.v2 import B2Api, Bucket, InMemoryAccountInfo, exception
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
# Import from b2_client to ensure timeout configuration is applied
|
||||
from .b2_client import B2Api, InMemoryAccountInfo
|
||||
from .const import (
|
||||
BACKBLAZE_REALM,
|
||||
CONF_APPLICATION_KEY,
|
||||
@@ -74,11 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_bucket_name",
|
||||
) from err
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
exception.ConnectionReset,
|
||||
) as err:
|
||||
except exception.ConnectionReset as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Backblaze B2 client with extended timeouts.
|
||||
|
||||
The b2sdk library uses class-level timeout attributes. To avoid modifying
|
||||
global library state, we subclass the relevant classes to provide extended
|
||||
timeouts suitable for backup operations involving large files.
|
||||
"""
|
||||
|
||||
from b2sdk.v2 import B2Api as BaseB2Api, InMemoryAccountInfo
|
||||
from b2sdk.v2.b2http import B2Http as BaseB2Http
|
||||
from b2sdk.v2.session import B2Session as BaseB2Session
|
||||
|
||||
# Extended timeouts for Home Assistant backup operations
|
||||
# Default CONNECTION_TIMEOUT is 46 seconds, which can be too short for slow connections
|
||||
CONNECTION_TIMEOUT = 120 # 2 minutes
|
||||
|
||||
# Default TIMEOUT_FOR_UPLOAD is 128 seconds, which is too short for large backups
|
||||
TIMEOUT_FOR_UPLOAD = 43200 # 12 hours
|
||||
|
||||
|
||||
class B2Http(BaseB2Http): # type: ignore[misc]
|
||||
"""B2Http with extended timeouts for backup operations."""
|
||||
|
||||
CONNECTION_TIMEOUT = CONNECTION_TIMEOUT
|
||||
TIMEOUT_FOR_UPLOAD = TIMEOUT_FOR_UPLOAD
|
||||
|
||||
|
||||
class B2Session(BaseB2Session): # type: ignore[misc]
|
||||
"""B2Session using custom B2Http with extended timeouts."""
|
||||
|
||||
B2HTTP_CLASS = B2Http
|
||||
|
||||
|
||||
class B2Api(BaseB2Api): # type: ignore[misc]
|
||||
"""B2Api using custom session with extended timeouts."""
|
||||
|
||||
SESSION_CLASS = B2Session
|
||||
|
||||
|
||||
__all__ = ["B2Api", "InMemoryAccountInfo"]
|
||||
@@ -36,10 +36,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Cache TTL for backup list (in seconds)
|
||||
CACHE_TTL = 300
|
||||
|
||||
# Timeout for upload operations (in seconds)
|
||||
# This prevents uploads from hanging indefinitely
|
||||
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
|
||||
|
||||
|
||||
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata files."""
|
||||
@@ -333,28 +329,13 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug("Uploading backup file %s with streaming", filename)
|
||||
try:
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
file_version = await asyncio.wait_for(
|
||||
self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
),
|
||||
timeout=UPLOAD_TIMEOUT,
|
||||
file_version = await self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
|
||||
)
|
||||
reader.abort()
|
||||
raise BackupAgentError(
|
||||
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
|
||||
) from None
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.warning("Upload of %s was cancelled", filename)
|
||||
reader.abort()
|
||||
raise
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from b2sdk.v2 import exception
|
||||
from b2sdk.v2 import B2Api, InMemoryAccountInfo, exception
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
@@ -17,8 +17,6 @@ from homeassistant.helpers.selector import (
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
# Import from b2_client to ensure timeout configuration is applied
|
||||
from .b2_client import B2Api, InMemoryAccountInfo
|
||||
from .const import (
|
||||
BACKBLAZE_REALM,
|
||||
CONF_APPLICATION_KEY,
|
||||
@@ -174,12 +172,8 @@ class BackblazeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
|
||||
)
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
exception.ConnectionReset,
|
||||
) as err:
|
||||
_LOGGER.error("Failed to connect to Backblaze B2: %s", err)
|
||||
except exception.ConnectionReset:
|
||||
_LOGGER.error("Failed to connect to Backblaze B2. Connection reset")
|
||||
errors["base"] = "cannot_connect"
|
||||
except exception.MissingAccountData:
|
||||
# This generally indicates an issue with how InMemoryAccountInfo is used
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["b2sdk"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["b2sdk==2.10.1"]
|
||||
"requirements": ["b2sdk==2.8.1"]
|
||||
}
|
||||
|
||||
@@ -34,12 +34,7 @@ class BeoData:
|
||||
|
||||
type BeoConfigEntry = ConfigEntry[BeoData]
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.EVENT,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Binary Sensor entities for the Bang & Olufsen integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mozart_api.models import BatteryState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BeoConfigEntry
|
||||
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
|
||||
from .entity import BeoEntity
|
||||
from .util import supports_battery
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BeoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Binary Sensor entities from config entry."""
|
||||
if await supports_battery(config_entry.runtime_data.client):
|
||||
async_add_entities(new_entities=[BeoBinarySensorBatteryCharging(config_entry)])
|
||||
|
||||
|
||||
class BeoBinarySensorBatteryCharging(BinarySensorEntity, BeoEntity):
|
||||
"""Battery charging Binary Sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
_attr_is_on = False
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Init the battery charging Binary Sensor."""
|
||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||
|
||||
self._attr_unique_id = f"{self._unique_id}_charging"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
self._update_battery_charging,
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_battery_charging(self, data: BatteryState) -> None:
|
||||
"""Update battery charging."""
|
||||
self._attr_is_on = bool(data.is_charging)
|
||||
self.async_write_ha_state()
|
||||
@@ -115,7 +115,6 @@ class WebsocketNotification(StrEnum):
|
||||
"""Enum for WebSocket notification types."""
|
||||
|
||||
ACTIVE_LISTENING_MODE = "active_listening_mode"
|
||||
BATTERY = "battery"
|
||||
BEO_REMOTE_BUTTON = "beo_remote_button"
|
||||
BUTTON = "button"
|
||||
PLAYBACK_ERROR = "playback_error"
|
||||
|
||||
@@ -4,10 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -57,19 +55,6 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
# Get remotes
|
||||
for remote in await get_remotes(config_entry.runtime_data.client):
|
||||
# Get Battery Sensor states
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{remote.serial_number}_{config_entry.unique_id}_remote_battery_level",
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data[f"remote_{remote.serial_number}_battery_level"] = state_dict
|
||||
|
||||
# Get key Event entity states (if enabled)
|
||||
for key_type in get_remote_keys():
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
@@ -87,26 +72,4 @@ async def async_get_config_entry_diagnostics(
|
||||
# Add remote Mozart model
|
||||
data[f"remote_{remote.serial_number}"] = dict(remote)
|
||||
|
||||
# Get Mozart battery entity
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_battery_level"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data["battery_level"] = state_dict
|
||||
|
||||
# Get Mozart battery charging entity
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_charging"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data["charging"] = state_dict
|
||||
|
||||
return data
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
"""Sensor entities for the Bang & Olufsen integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from mozart_api.exceptions import ApiException
|
||||
from mozart_api.models import BatteryState, PairedRemote
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BeoConfigEntry
|
||||
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
|
||||
from .entity import BeoEntity
|
||||
from .util import get_remotes, supports_battery
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BeoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sensor entities from config entry."""
|
||||
entities: list[BeoSensor] = []
|
||||
|
||||
# Check for Mozart device with battery
|
||||
if await supports_battery(config_entry.runtime_data.client):
|
||||
entities.append(BeoSensorBatteryLevel(config_entry))
|
||||
|
||||
# Add any Beoremote One remotes
|
||||
entities.extend(
|
||||
[
|
||||
BeoSensorRemoteBatteryLevel(config_entry, remote)
|
||||
for remote in (await get_remotes(config_entry.runtime_data.client))
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class BeoSensor(SensorEntity, BeoEntity):
|
||||
"""Base Bang & Olufsen Sensor."""
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Initialize Sensor."""
|
||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||
|
||||
|
||||
class BeoSensorBatteryLevel(BeoSensor):
|
||||
"""Battery level Sensor for Mozart devices."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Init the battery level Sensor."""
|
||||
super().__init__(config_entry)
|
||||
|
||||
self._attr_unique_id = f"{self._unique_id}_battery_level"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
self._update_battery,
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_battery(self, data: BatteryState) -> None:
|
||||
"""Update sensor value."""
|
||||
self._attr_native_value = data.battery_level
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class BeoSensorRemoteBatteryLevel(BeoSensor):
|
||||
"""Battery level Sensor for the Beoremote One."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_should_poll = True
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry, remote: PairedRemote) -> None:
|
||||
"""Init the battery level Sensor."""
|
||||
super().__init__(config_entry)
|
||||
# Serial number is not None, as the remote object is provided by get_remotes
|
||||
assert remote.serial_number
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{remote.serial_number}_{self._unique_id}_remote_battery_level"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")}
|
||||
)
|
||||
self._attr_native_value = remote.battery_level
|
||||
self._remote = remote
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Poll battery status."""
|
||||
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
|
||||
for remote in await get_remotes(self._client):
|
||||
if remote.serial_number == self._remote.serial_number:
|
||||
self._attr_native_value = remote.battery_level
|
||||
break
|
||||
@@ -84,10 +84,3 @@ def get_remote_keys() -> list[str]:
|
||||
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
async def supports_battery(client: MozartClient) -> bool:
|
||||
"""Get if a Mozart device has a battery."""
|
||||
battery_state = await client.get_battery_state()
|
||||
|
||||
return battery_state.state != "BatteryNotPresent"
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mozart_api.models import (
|
||||
BatteryState,
|
||||
BeoRemoteButton,
|
||||
ButtonEvent,
|
||||
ListeningModeProps,
|
||||
@@ -61,7 +60,6 @@ class BeoWebsocket(BeoBase):
|
||||
self._client.get_active_listening_mode_notifications(
|
||||
self.on_active_listening_mode
|
||||
)
|
||||
self._client.get_battery_notifications(self.on_battery_notification)
|
||||
self._client.get_beo_remote_button_notifications(
|
||||
self.on_beo_remote_button_notification
|
||||
)
|
||||
@@ -117,14 +115,6 @@ class BeoWebsocket(BeoBase):
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_battery_notification(self, notification: BatteryState) -> None:
|
||||
"""Send battery dispatch."""
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None:
|
||||
"""Send beo_remote_button dispatch."""
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "blackbird",
|
||||
"name": "Monoprice Blackbird Matrix Switch",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it references pyserial-asyncio, which does blocking I/O in the asyncio loop and is not maintained.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/blackbird",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyblackbird"],
|
||||
|
||||
@@ -2,25 +2,16 @@
|
||||
|
||||
from pyblu import Player
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
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 (
|
||||
ATTR_MASTER,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SET_TIMER,
|
||||
SERVICE_UNJOIN,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
BluesoundConfigEntry,
|
||||
BluesoundCoordinator,
|
||||
@@ -37,38 +28,6 @@ PLATFORMS = [
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Bluesound."""
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_TIMER,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_increase_timer",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_clear_timer",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_JOIN,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema={vol.Required(ATTR_MASTER): cv.entity_id},
|
||||
func="async_bluesound_join",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_UNJOIN,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_bluesound_unjoin",
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,3 @@ DOMAIN = "bluesound"
|
||||
INTEGRATION_TITLE = "Bluesound"
|
||||
ATTR_BLUESOUND_GROUP = "bluesound_group"
|
||||
ATTR_MASTER = "master"
|
||||
|
||||
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyblu import Input, Player, Preset, Status, SyncStatus
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -21,7 +22,12 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
@@ -35,15 +41,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import (
|
||||
ATTR_BLUESOUND_GROUP,
|
||||
ATTR_MASTER,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SET_TIMER,
|
||||
SERVICE_UNJOIN,
|
||||
)
|
||||
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
||||
from .coordinator import BluesoundCoordinator
|
||||
from .utils import (
|
||||
dispatcher_join_signal,
|
||||
@@ -62,6 +60,11 @@ SCAN_INTERVAL = timedelta(minutes=15)
|
||||
DATA_BLUESOUND = DOMAIN
|
||||
DEFAULT_PORT = 11000
|
||||
|
||||
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
POLL_TIMEOUT = 120
|
||||
|
||||
|
||||
@@ -78,6 +81,20 @@ async def async_setup_entry(
|
||||
config_entry.runtime_data.player,
|
||||
)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_TIMER, None, "async_increase_timer"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_CLEAR_TIMER, None, "async_clear_timer"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, "async_bluesound_join"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_UNJOIN, None, "async_bluesound_unjoin"
|
||||
)
|
||||
|
||||
async_add_entities([bluesound_player], update_before_add=True)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_USE_SSL
|
||||
from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator
|
||||
|
||||
PLATFORMS: Final[list[Platform]] = [
|
||||
@@ -27,12 +26,11 @@ async def async_setup_entry(
|
||||
"""Set up a config entry."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
mac = config_entry.data[CONF_MAC]
|
||||
ssl = config_entry.data.get(CONF_USE_SSL, False)
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
|
||||
)
|
||||
client = BraviaClient(host, mac, session=session, ssl=ssl)
|
||||
client = BraviaClient(host, mac, session=session)
|
||||
coordinator = BraviaTVCoordinator(
|
||||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
|
||||
@@ -28,7 +28,6 @@ from .const import (
|
||||
ATTR_MODEL,
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
DOMAIN,
|
||||
NICKNAME_PREFIX,
|
||||
)
|
||||
@@ -47,12 +46,11 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def create_client(self) -> None:
|
||||
"""Create Bravia TV client from config."""
|
||||
host = self.device_config[CONF_HOST]
|
||||
ssl = self.device_config[CONF_USE_SSL]
|
||||
session = async_create_clientsession(
|
||||
self.hass,
|
||||
cookie_jar=CookieJar(unsafe=True, quote_cookie=False),
|
||||
)
|
||||
self.client = BraviaClient(host=host, session=session, ssl=ssl)
|
||||
self.client = BraviaClient(host=host, session=session)
|
||||
|
||||
async def gen_instance_ids(self) -> tuple[str, str]:
|
||||
"""Generate client_id and nickname."""
|
||||
@@ -125,10 +123,10 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle authorize step."""
|
||||
self.create_client()
|
||||
|
||||
if user_input is not None:
|
||||
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
|
||||
self.device_config[CONF_USE_SSL] = user_input[CONF_USE_SSL]
|
||||
self.create_client()
|
||||
if user_input[CONF_USE_PSK]:
|
||||
return await self.async_step_psk()
|
||||
return await self.async_step_pin()
|
||||
@@ -138,7 +136,6 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USE_PSK, default=False): bool,
|
||||
vol.Required(CONF_USE_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
CONF_USE_PSK: Final = "use_psk"
|
||||
CONF_USE_SSL: Final = "use_ssl"
|
||||
|
||||
DOMAIN: Final = "braviatv"
|
||||
LEGACY_CLIENT_ID: Final = "HomeAssistant"
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.components.media_player import MediaType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -56,31 +56,8 @@ def catch_braviatv_errors[_BraviaTVCoordinatorT: BraviaTVCoordinator, **_P](
|
||||
"""Catch Bravia errors and log message."""
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except BraviaNotFound as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error_not_found",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error_offline",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except BraviaError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
_LOGGER.error("Command error: %s", err)
|
||||
await self.async_request_refresh()
|
||||
|
||||
return wrapper
|
||||
@@ -188,35 +165,17 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
|
||||
if self.skipped_updates < 10:
|
||||
self.connected = False
|
||||
self.skipped_updates += 1
|
||||
_LOGGER.debug(
|
||||
"Update for %s skipped: the Bravia API service is reloading",
|
||||
self.config_entry.title,
|
||||
)
|
||||
_LOGGER.debug("Update skipped, Bravia API service is reloading")
|
||||
return
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error_not_found",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
raise UpdateFailed("Error communicating with device") from err
|
||||
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff):
|
||||
self.is_on = False
|
||||
self.connected = False
|
||||
_LOGGER.debug(
|
||||
"Update for %s skipped: the TV is turned off", self.config_entry.title
|
||||
)
|
||||
_LOGGER.debug("Update skipped, Bravia TV is off")
|
||||
except BraviaError as err:
|
||||
self.is_on = False
|
||||
self.connected = False
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
raise UpdateFailed("Error communicating with device") from err
|
||||
|
||||
async def async_update_volume(self) -> None:
|
||||
"""Update volume information."""
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pybravia"],
|
||||
"requirements": ["pybravia==0.4.1"],
|
||||
"requirements": ["pybravia==0.3.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Sony Corporation",
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
"step": {
|
||||
"authorize": {
|
||||
"data": {
|
||||
"use_psk": "Use PSK authentication",
|
||||
"use_ssl": "Use SSL connection"
|
||||
"use_psk": "Use PSK authentication"
|
||||
},
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV. Go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended, as it is more stable. \n\nUse an SSL connection only if your TV supports this connection type.",
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
|
||||
"title": "Authorize Sony Bravia TV"
|
||||
},
|
||||
"confirm": {
|
||||
@@ -55,22 +54,5 @@
|
||||
"name": "Terminate apps"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"command_error": {
|
||||
"message": "Error sending command to {device}: {error}"
|
||||
},
|
||||
"command_error_not_found": {
|
||||
"message": "Error sending command to {device}: the Bravia API service is reloading"
|
||||
},
|
||||
"command_error_offline": {
|
||||
"message": "Error sending command to {device}: the TV is turned off"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "Error updating data for {device}: {error}"
|
||||
},
|
||||
"update_error_not_found": {
|
||||
"message": "Error updating data for {device}: the Bravia API service is stuck"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,18 +27,13 @@ from homeassistant.exceptions import (
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
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 .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type BSBLanConfigEntry = ConfigEntry[BSBLanData]
|
||||
|
||||
|
||||
@@ -54,12 +49,6 @@ class BSBLanData:
|
||||
static: StaticState
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the BSB-Lan integration."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bool:
|
||||
"""Set up BSB-Lan from a config entry."""
|
||||
|
||||
|
||||
@@ -111,17 +111,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
return None
|
||||
return self.coordinator.data.state.target_temperature.value
|
||||
|
||||
@property
|
||||
def _hvac_mode_value(self) -> int | str | None:
|
||||
"""Return the raw hvac_mode value from the coordinator."""
|
||||
if (hvac_mode := self.coordinator.data.state.hvac_mode) is None:
|
||||
return None
|
||||
return hvac_mode.value
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
if (hvac_mode_value := self._hvac_mode_value) is None:
|
||||
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
|
||||
if hvac_mode_value is None:
|
||||
return None
|
||||
# BSB-Lan returns integer values: 0=off, 1=auto, 2=eco, 3=heat
|
||||
if isinstance(hvac_mode_value, int):
|
||||
@@ -131,8 +125,9 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
|
||||
# BSB-Lan mode 2 is eco/reduced mode
|
||||
if self._hvac_mode_value == 2:
|
||||
if hvac_mode_value == 2:
|
||||
return PRESET_ECO
|
||||
return PRESET_NONE
|
||||
|
||||
|
||||
@@ -29,11 +29,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
|
||||
name=data.device.name,
|
||||
manufacturer="BSBLAN Inc.",
|
||||
model=(
|
||||
data.info.device_identification.value
|
||||
if data.info.device_identification
|
||||
else None
|
||||
),
|
||||
model=data.info.device_identification.value,
|
||||
sw_version=data.device.version,
|
||||
configuration_url=f"http://{host}",
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"services": {
|
||||
"set_hot_water_schedule": {
|
||||
"service": "mdi:calendar-clock"
|
||||
},
|
||||
"sync_time": {
|
||||
"service": "mdi:timer-sync-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==4.1.0"],
|
||||
"requirements": ["python-bsblan==3.1.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
"""Support for BSB-Lan services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import time
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import BSBLanConfigEntry
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_MONDAY_SLOTS = "monday_slots"
|
||||
ATTR_TUESDAY_SLOTS = "tuesday_slots"
|
||||
ATTR_WEDNESDAY_SLOTS = "wednesday_slots"
|
||||
ATTR_THURSDAY_SLOTS = "thursday_slots"
|
||||
ATTR_FRIDAY_SLOTS = "friday_slots"
|
||||
ATTR_SATURDAY_SLOTS = "saturday_slots"
|
||||
ATTR_SUNDAY_SLOTS = "sunday_slots"
|
||||
|
||||
# Service names
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE = "set_hot_water_schedule"
|
||||
SERVICE_SYNC_TIME = "sync_time"
|
||||
|
||||
|
||||
# Schema for a single time slot
|
||||
_SLOT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("start_time"): cv.time,
|
||||
vol.Required("end_time"): cv.time,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
vol.Optional(ATTR_MONDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_TUESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_WEDNESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_THURSDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_FRIDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_SATURDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_SUNDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _convert_time_slots_to_day_schedule(
|
||||
slots: list[dict[str, time]] | None,
|
||||
) -> DaySchedule | None:
|
||||
"""Convert list of time slot dicts to a DaySchedule object.
|
||||
|
||||
Example: [{"start_time": time(6, 0), "end_time": time(8, 0)},
|
||||
{"start_time": time(17, 0), "end_time": time(21, 0)}]
|
||||
becomes: DaySchedule with two TimeSlot objects
|
||||
|
||||
None returns None (don't modify this day).
|
||||
Empty list returns DaySchedule with empty slots (clear this day).
|
||||
"""
|
||||
if slots is None:
|
||||
return None
|
||||
|
||||
if not slots:
|
||||
return DaySchedule(slots=[])
|
||||
|
||||
time_slots = []
|
||||
for slot in slots:
|
||||
start_time = slot["start_time"]
|
||||
end_time = slot["end_time"]
|
||||
|
||||
# Validate that end time is after start time
|
||||
if end_time <= start_time:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="end_time_before_start_time",
|
||||
translation_placeholders={
|
||||
"start_time": start_time.strftime("%H:%M"),
|
||||
"end_time": end_time.strftime("%H:%M"),
|
||||
},
|
||||
)
|
||||
|
||||
time_slots.append(TimeSlot(start=start_time, end=end_time))
|
||||
LOGGER.debug(
|
||||
"Created time slot: %s-%s",
|
||||
start_time.strftime("%H:%M"),
|
||||
end_time.strftime("%H:%M"),
|
||||
)
|
||||
|
||||
LOGGER.debug("Created DaySchedule with %d slots", len(time_slots))
|
||||
return DaySchedule(slots=time_slots)
|
||||
|
||||
|
||||
async def set_hot_water_schedule(service_call: ServiceCall) -> None:
|
||||
"""Set hot water heating schedule."""
|
||||
device_id = service_call.data[ATTR_DEVICE_ID]
|
||||
|
||||
# Get the device and config entry
|
||||
device_registry = dr.async_get(service_call.hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
# Find the config entry for this device
|
||||
matching_entries: list[BSBLanConfigEntry] = [
|
||||
entry
|
||||
for entry in service_call.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
]
|
||||
|
||||
if not matching_entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_config_entry_for_device",
|
||||
translation_placeholders={"device_id": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
entry = matching_entries[0]
|
||||
|
||||
# Verify the config entry is loaded
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_loaded",
|
||||
translation_placeholders={"device_name": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
client = entry.runtime_data.client
|
||||
|
||||
# Convert time slots to DaySchedule objects
|
||||
monday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_MONDAY_SLOTS)
|
||||
)
|
||||
tuesday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_TUESDAY_SLOTS)
|
||||
)
|
||||
wednesday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_WEDNESDAY_SLOTS)
|
||||
)
|
||||
thursday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_THURSDAY_SLOTS)
|
||||
)
|
||||
friday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_FRIDAY_SLOTS)
|
||||
)
|
||||
saturday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_SATURDAY_SLOTS)
|
||||
)
|
||||
sunday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_SUNDAY_SLOTS)
|
||||
)
|
||||
|
||||
# Create the DHWSchedule object
|
||||
dhw_schedule = DHWSchedule(
|
||||
monday=monday,
|
||||
tuesday=tuesday,
|
||||
wednesday=wednesday,
|
||||
thursday=thursday,
|
||||
friday=friday,
|
||||
saturday=saturday,
|
||||
sunday=sunday,
|
||||
)
|
||||
|
||||
LOGGER.debug(
|
||||
"Setting hot water schedule - Monday: %s, Tuesday: %s, Wednesday: %s, "
|
||||
"Thursday: %s, Friday: %s, Saturday: %s, Sunday: %s",
|
||||
monday,
|
||||
tuesday,
|
||||
wednesday,
|
||||
thursday,
|
||||
friday,
|
||||
saturday,
|
||||
sunday,
|
||||
)
|
||||
|
||||
try:
|
||||
# Call the BSB-Lan API to set the schedule
|
||||
await client.set_hot_water_schedule(dhw_schedule)
|
||||
except BSBLANError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_schedule_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
# Refresh the slow coordinator to get the updated schedule
|
||||
await entry.runtime_data.slow_coordinator.async_request_refresh()
|
||||
|
||||
|
||||
async def async_sync_time(service_call: ServiceCall) -> None:
|
||||
"""Synchronize BSB-LAN device time with Home Assistant."""
|
||||
device_id: str = service_call.data[ATTR_DEVICE_ID]
|
||||
|
||||
# Get the device and config entry
|
||||
device_registry = dr.async_get(service_call.hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
# Find the config entry for this device
|
||||
matching_entries: list[BSBLanConfigEntry] = [
|
||||
entry
|
||||
for entry in service_call.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
]
|
||||
|
||||
if not matching_entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_config_entry_for_device",
|
||||
translation_placeholders={"device_id": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
entry = matching_entries[0]
|
||||
|
||||
# Verify the config entry is loaded
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_loaded",
|
||||
translation_placeholders={"device_name": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
client = entry.runtime_data.client
|
||||
|
||||
try:
|
||||
# Get current device time
|
||||
device_time = await client.time()
|
||||
current_time = dt_util.now()
|
||||
current_time_str = current_time.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
# Only sync if device time differs from HA time
|
||||
if device_time.time.value != current_time_str:
|
||||
await client.set_time(current_time_str)
|
||||
except BSBLANError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="sync_time_failed",
|
||||
translation_placeholders={
|
||||
"device_name": device_entry.name or device_id,
|
||||
"error": str(err),
|
||||
},
|
||||
) from err
|
||||
|
||||
|
||||
SYNC_TIME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the BSB-Lan services."""
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
set_hot_water_schedule,
|
||||
schema=SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SYNC_TIME,
|
||||
async_sync_time,
|
||||
schema=SYNC_TIME_SCHEMA,
|
||||
)
|
||||
@@ -1,122 +0,0 @@
|
||||
sync_time:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
example: "abc123device456"
|
||||
selector:
|
||||
device:
|
||||
integration: bsblan
|
||||
|
||||
set_hot_water_schedule:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
example: "abc123device456"
|
||||
selector:
|
||||
device:
|
||||
integration: bsblan
|
||||
monday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
tuesday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
wednesday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
thursday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
friday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
saturday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
sunday_slots:
|
||||
selector:
|
||||
object:
|
||||
multiple: true
|
||||
label_field: start_time
|
||||
description_field: end_time
|
||||
fields:
|
||||
start_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
selector:
|
||||
time:
|
||||
@@ -70,18 +70,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_loaded": {
|
||||
"message": "The device `{device_name}` is not currently loaded or available"
|
||||
},
|
||||
"end_time_before_start_time": {
|
||||
"message": "End time ({end_time}) must be after start time ({start_time})"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID: {device_id}"
|
||||
},
|
||||
"no_config_entry_for_device": {
|
||||
"message": "No configuration entry found for device: {device_id}"
|
||||
},
|
||||
"set_data_error": {
|
||||
"message": "An error occurred while sending the data to the BSB-Lan device"
|
||||
},
|
||||
@@ -91,9 +79,6 @@
|
||||
"set_preset_mode_error": {
|
||||
"message": "Can't set preset mode to {preset_mode} when HVAC mode is not set to auto"
|
||||
},
|
||||
"set_schedule_failed": {
|
||||
"message": "Failed to set hot water schedule: {error}"
|
||||
},
|
||||
"set_temperature_error": {
|
||||
"message": "An error occurred while setting the temperature"
|
||||
},
|
||||
@@ -105,59 +90,6 @@
|
||||
},
|
||||
"setup_general_error": {
|
||||
"message": "An unknown error occurred while retrieving static device data"
|
||||
},
|
||||
"sync_time_failed": {
|
||||
"message": "Failed to sync time for {device_name}: {error}"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_hot_water_schedule": {
|
||||
"description": "Set the hot water heating schedule for a BSB-LAN device.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "The BSB-LAN device to configure.",
|
||||
"name": "Device"
|
||||
},
|
||||
"friday_slots": {
|
||||
"description": "Time periods for Friday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Friday time slots"
|
||||
},
|
||||
"monday_slots": {
|
||||
"description": "Time periods for Monday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Monday time slots"
|
||||
},
|
||||
"saturday_slots": {
|
||||
"description": "Time periods for Saturday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Saturday time slots"
|
||||
},
|
||||
"sunday_slots": {
|
||||
"description": "Time periods for Sunday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Sunday time slots"
|
||||
},
|
||||
"thursday_slots": {
|
||||
"description": "Time periods for Thursday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Thursday time slots"
|
||||
},
|
||||
"tuesday_slots": {
|
||||
"description": "Time periods for Tuesday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Tuesday time slots"
|
||||
},
|
||||
"wednesday_slots": {
|
||||
"description": "Time periods for Wednesday. Add multiple slots for different heating periods throughout the day.",
|
||||
"name": "Wednesday time slots"
|
||||
}
|
||||
},
|
||||
"name": "Set hot water schedule"
|
||||
},
|
||||
"sync_time": {
|
||||
"description": "Synchronize Home Assistant time to the BSB-Lan device. Only updates if device time differs from Home Assistant time.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "The BSB-LAN device to sync time for.",
|
||||
"name": "Device"
|
||||
}
|
||||
},
|
||||
"name": "Sync time"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.16.0"]
|
||||
"requirements": ["bthome-ble==3.17.0"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
|
||||
cv.ensure_list, vol.Length(min=1), [HVACMode]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -31,11 +27,14 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -50,6 +50,7 @@ from . import (
|
||||
from .client import CloudClient
|
||||
from .const import (
|
||||
CONF_ACCOUNT_LINK_SERVER,
|
||||
CONF_ACCOUNTS_SERVER,
|
||||
CONF_ACME_SERVER,
|
||||
CONF_ALEXA,
|
||||
CONF_ALIASES,
|
||||
@@ -137,6 +138,7 @@ _BASE_CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
|
||||
vol.Optional(CONF_ACCOUNTS_SERVER): str,
|
||||
vol.Optional(CONF_ACME_SERVER): str,
|
||||
vol.Optional(CONF_API_SERVER): str,
|
||||
vol.Optional(CONF_RELAYER_SERVER): str,
|
||||
|
||||
@@ -76,6 +76,7 @@ CONF_GOOGLE_ACTIONS = "google_actions"
|
||||
CONF_USER_POOL_ID = "user_pool_id"
|
||||
|
||||
CONF_ACCOUNT_LINK_SERVER = "account_link_server"
|
||||
CONF_ACCOUNTS_SERVER = "accounts_server"
|
||||
CONF_ACME_SERVER = "acme_server"
|
||||
CONF_API_SERVER = "api_server"
|
||||
CONF_DISCOVERY_SERVICE_ACTIONS = "discovery_service_actions"
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.9.0"],
|
||||
"requirements": ["hass-nabucasa==1.7.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from aiocomelit.const import BRIDGE
|
||||
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_VEDO_PIN, DEFAULT_PORT
|
||||
from .const import DEFAULT_PORT
|
||||
from .coordinator import (
|
||||
ComelitBaseCoordinator,
|
||||
ComelitConfigEntry,
|
||||
@@ -22,16 +22,6 @@ BRIDGE_PLATFORMS = [
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
BRIDGE_AND_VEDO_PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.LIGHT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
VEDO_PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -47,20 +37,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
session = await async_client_session(hass)
|
||||
|
||||
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
vedo_pin = entry.data.get(CONF_VEDO_PIN)
|
||||
coordinator = ComelitSerialBridge(
|
||||
hass,
|
||||
entry,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_PORT, DEFAULT_PORT),
|
||||
entry.data[CONF_PIN],
|
||||
vedo_pin,
|
||||
session,
|
||||
)
|
||||
platforms = BRIDGE_PLATFORMS
|
||||
# Add VEDO platforms if vedo_pin is configured
|
||||
if vedo_pin:
|
||||
platforms = BRIDGE_AND_VEDO_PLATFORMS
|
||||
else:
|
||||
coordinator = ComelitVedoSystem(
|
||||
hass,
|
||||
@@ -86,9 +71,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) ->
|
||||
|
||||
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
platforms = BRIDGE_PLATFORMS
|
||||
# Add VEDO platforms if vedo_pin was configured
|
||||
if entry.data.get(CONF_VEDO_PIN):
|
||||
platforms = BRIDGE_AND_VEDO_PLATFORMS
|
||||
else:
|
||||
platforms = VEDO_PLATFORMS
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import cast
|
||||
|
||||
from aiocomelit.api import ComelitVedoAreaObject
|
||||
from aiocomelit.const import ALARM_AREA, AlarmAreaState
|
||||
from aiocomelit.const import AlarmAreaState
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
|
||||
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -56,25 +56,15 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Comelit VEDO system alarm control panel devices."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
is_bridge = isinstance(coordinator, ComelitSerialBridge)
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if is_bridge:
|
||||
assert isinstance(coordinator, ComelitSerialBridge)
|
||||
else:
|
||||
assert isinstance(coordinator, ComelitVedoSystem)
|
||||
|
||||
if data := coordinator.data[ALARM_AREA]:
|
||||
async_add_entities(
|
||||
ComelitAlarmEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in data.values()
|
||||
)
|
||||
async_add_entities(
|
||||
ComelitAlarmEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data["alarm_areas"].values()
|
||||
)
|
||||
|
||||
|
||||
class ComelitAlarmEntity(
|
||||
CoordinatorEntity[ComelitVedoSystem | ComelitSerialBridge], AlarmControlPanelEntity
|
||||
):
|
||||
class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanelEntity):
|
||||
"""Representation of a Ness alarm panel."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
@@ -88,7 +78,7 @@ class ComelitAlarmEntity(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ComelitVedoSystem | ComelitSerialBridge,
|
||||
coordinator: ComelitVedoSystem,
|
||||
area: ComelitVedoAreaObject,
|
||||
config_entry_entry_id: str,
|
||||
) -> None:
|
||||
@@ -105,9 +95,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def _area(self) -> ComelitVedoAreaObject:
|
||||
"""Return area object."""
|
||||
return cast(
|
||||
ComelitVedoAreaObject, self.coordinator.data[ALARM_AREA][self._area_index]
|
||||
)
|
||||
return self.coordinator.data["alarm_areas"][self._area_index]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import cast
|
||||
|
||||
from aiocomelit.api import ComelitVedoZoneObject
|
||||
from aiocomelit.const import ALARM_ZONE, AlarmZoneState
|
||||
from aiocomelit import ComelitVedoZoneObject
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -16,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ObjectClassType
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
|
||||
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
|
||||
from .utils import new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
@@ -30,32 +29,25 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Comelit VEDO presence sensors."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
is_bridge = isinstance(coordinator, ComelitSerialBridge)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if is_bridge:
|
||||
assert isinstance(coordinator, ComelitSerialBridge)
|
||||
else:
|
||||
assert isinstance(coordinator, ComelitVedoSystem)
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, ALARM_ZONE)
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
)
|
||||
|
||||
|
||||
class ComelitVedoBinarySensorEntity(
|
||||
CoordinatorEntity[ComelitVedoSystem | ComelitSerialBridge], BinarySensorEntity
|
||||
CoordinatorEntity[ComelitVedoSystem], BinarySensorEntity
|
||||
):
|
||||
"""Sensor device."""
|
||||
|
||||
@@ -64,7 +56,7 @@ class ComelitVedoBinarySensorEntity(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ComelitVedoSystem | ComelitSerialBridge,
|
||||
coordinator: ComelitVedoSystem,
|
||||
zone: ComelitVedoZoneObject,
|
||||
config_entry_entry_id: str,
|
||||
) -> None:
|
||||
@@ -76,25 +68,9 @@ class ComelitVedoBinarySensorEntity(
|
||||
self._attr_unique_id = f"{config_entry_entry_id}-presence-{zone.index}"
|
||||
self._attr_device_info = coordinator.platform_device_info(zone, "zone")
|
||||
|
||||
@property
|
||||
def _zone(self) -> ComelitVedoZoneObject:
|
||||
"""Return zone object."""
|
||||
return cast(
|
||||
ComelitVedoZoneObject, self.coordinator.data[ALARM_ZONE][self._zone_index]
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._zone.human_status in [
|
||||
AlarmZoneState.FAULTY,
|
||||
AlarmZoneState.UNAVAILABLE,
|
||||
AlarmZoneState.UNKNOWN,
|
||||
]:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Presence detected."""
|
||||
return self._zone.status_api == "0001"
|
||||
return (
|
||||
self.coordinator.data["alarm_zones"][self._zone_index].status_api == "0001"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import (
|
||||
ComeliteSerialBridgeApi,
|
||||
@@ -22,7 +22,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import _LOGGER, CONF_VEDO_PIN, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
|
||||
from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
|
||||
from .utils import async_client_session
|
||||
|
||||
DEFAULT_HOST = "192.168.1.252"
|
||||
@@ -34,12 +34,9 @@ USER_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
|
||||
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
||||
vol.Optional(CONF_VEDO_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_PIN): cv.string, vol.Optional(CONF_VEDO_PIN): cv.string}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
|
||||
@@ -75,18 +72,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
finally:
|
||||
await api.logout()
|
||||
|
||||
# Validate VEDO PIN if provided and device type is BRIDGE
|
||||
if data.get(CONF_VEDO_PIN) and data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_VEDO_PIN]):
|
||||
raise InvalidVedoPin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(api, ComeliteSerialBridgeApi)
|
||||
|
||||
# Verify VEDO is enabled with the provided PIN
|
||||
if not await api.vedo_enabled(data[CONF_VEDO_PIN]):
|
||||
raise InvalidVedoAuth
|
||||
|
||||
return {"title": data[CONF_HOST]}
|
||||
|
||||
|
||||
@@ -114,10 +99,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except InvalidVedoPin:
|
||||
errors["base"] = "invalid_vedo_pin"
|
||||
except InvalidVedoAuth:
|
||||
errors["base"] = "invalid_vedo_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -201,8 +182,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PIN: user_input[CONF_PIN],
|
||||
CONF_TYPE: reconfigure_entry.data.get(CONF_TYPE, BRIDGE),
|
||||
}
|
||||
if CONF_VEDO_PIN in user_input:
|
||||
data_to_validate[CONF_VEDO_PIN] = user_input[CONF_VEDO_PIN]
|
||||
await validate_input(self.hass, data_to_validate)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
@@ -210,10 +189,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except InvalidVedoPin:
|
||||
errors["base"] = "invalid_vedo_pin"
|
||||
except InvalidVedoAuth:
|
||||
errors["base"] = "invalid_vedo_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -223,8 +198,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_PIN: user_input[CONF_PIN],
|
||||
}
|
||||
if CONF_VEDO_PIN in user_input:
|
||||
data_updates[CONF_VEDO_PIN] = user_input[CONF_VEDO_PIN]
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry, data_updates=data_updates
|
||||
)
|
||||
@@ -238,7 +211,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PORT, default=reconfigure_entry.data[CONF_PORT]
|
||||
): cv.port,
|
||||
vol.Optional(CONF_PIN): cv.string,
|
||||
vol.Optional(CONF_VEDO_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -259,11 +231,3 @@ class InvalidAuth(HomeAssistantError):
|
||||
|
||||
class InvalidPin(HomeAssistantError):
|
||||
"""Error to indicate an invalid pin."""
|
||||
|
||||
|
||||
class InvalidVedoPin(HomeAssistantError):
|
||||
"""Error to indicate an invalid VEDO pin."""
|
||||
|
||||
|
||||
class InvalidVedoAuth(HomeAssistantError):
|
||||
"""Error to indicate VEDO authentication failed."""
|
||||
|
||||
@@ -19,7 +19,6 @@ ObjectClassType = (
|
||||
DOMAIN = "comelit"
|
||||
DEFAULT_PORT = 80
|
||||
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
|
||||
CONF_VEDO_PIN = "vedo_pin"
|
||||
|
||||
SCAN_INTERVAL = 5
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"""Support for Comelit."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
from typing import TypeVar, cast
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from aiocomelit.api import ComelitCommonApi, ComeliteSerialBridgeApi, ComelitVedoApi
|
||||
from aiocomelit.api import (
|
||||
AlarmDataObject,
|
||||
ComelitCommonApi,
|
||||
ComeliteSerialBridgeApi,
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoApi,
|
||||
)
|
||||
from aiocomelit.const import (
|
||||
ALARM_AREA,
|
||||
ALARM_ZONE,
|
||||
BRIDGE,
|
||||
CLIMATE,
|
||||
COVER,
|
||||
@@ -34,10 +37,7 @@ type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
|
||||
|
||||
T = TypeVar(
|
||||
"T",
|
||||
bound=dict[
|
||||
str,
|
||||
Mapping[int, ObjectClassType],
|
||||
],
|
||||
bound=dict[str, dict[int, ComelitSerialBridgeObject]] | AlarmDataObject,
|
||||
)
|
||||
|
||||
|
||||
@@ -118,8 +118,8 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
|
||||
async def _async_remove_stale_devices(
|
||||
self,
|
||||
previous_list: Mapping[int, ObjectClassType],
|
||||
current_list: Mapping[int, ObjectClassType],
|
||||
previous_list: dict[int, Any],
|
||||
current_list: dict[int, Any],
|
||||
dev_type: str,
|
||||
) -> None:
|
||||
"""Remove stale devices."""
|
||||
@@ -143,7 +143,9 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
)
|
||||
|
||||
|
||||
class ComelitSerialBridge(ComelitBaseCoordinator[T]):
|
||||
class ComelitSerialBridge(
|
||||
ComelitBaseCoordinator[dict[str, dict[int, ComelitSerialBridgeObject]]]
|
||||
):
|
||||
"""Queries Comelit Serial Bridge."""
|
||||
|
||||
_hw_version = "20003101"
|
||||
@@ -156,23 +158,17 @@ class ComelitSerialBridge(ComelitBaseCoordinator[T]):
|
||||
host: str,
|
||||
port: int,
|
||||
pin: str,
|
||||
vedo_pin: str | None,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.api = ComeliteSerialBridgeApi(host, port, pin, session)
|
||||
self.vedo_pin = vedo_pin
|
||||
super().__init__(hass, entry, BRIDGE, host)
|
||||
|
||||
async def _async_update_system_data(
|
||||
self,
|
||||
) -> T:
|
||||
) -> dict[str, dict[int, ComelitSerialBridgeObject]]:
|
||||
"""Specific method for updating data."""
|
||||
data: dict[
|
||||
str,
|
||||
Mapping[int, ObjectClassType],
|
||||
] = {}
|
||||
data.update(await self.api.get_all_devices())
|
||||
data = await self.api.get_all_devices()
|
||||
|
||||
if self.data:
|
||||
for dev_type in (CLIMATE, COVER, LIGHT, IRRIGATION, OTHER, SCENARIO):
|
||||
@@ -180,14 +176,10 @@ class ComelitSerialBridge(ComelitBaseCoordinator[T]):
|
||||
self.data[dev_type], data[dev_type], dev_type
|
||||
)
|
||||
|
||||
# Get VEDO alarm data if vedo_pin is configured
|
||||
if self.vedo_pin:
|
||||
data.update(await self.api.get_all_areas_and_zones())
|
||||
|
||||
return cast(T, data)
|
||||
return data
|
||||
|
||||
|
||||
class ComelitVedoSystem(ComelitBaseCoordinator[T]):
|
||||
class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
|
||||
"""Queries Comelit VEDO system."""
|
||||
|
||||
_hw_version = "VEDO IP"
|
||||
@@ -204,21 +196,20 @@ class ComelitVedoSystem(ComelitBaseCoordinator[T]):
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.api = ComelitVedoApi(host, port, pin, session)
|
||||
self.vedo_pin = pin
|
||||
super().__init__(hass, entry, VEDO, host)
|
||||
|
||||
async def _async_update_system_data(
|
||||
self,
|
||||
) -> T:
|
||||
) -> AlarmDataObject:
|
||||
"""Specific method for updating data."""
|
||||
data = await self.api.get_all_areas_and_zones()
|
||||
|
||||
if self.data:
|
||||
for obj_type in (ALARM_AREA, ALARM_ZONE):
|
||||
for obj_type in ("alarm_areas", "alarm_zones"):
|
||||
await self._async_remove_stale_devices(
|
||||
self.data[obj_type],
|
||||
data[obj_type],
|
||||
"area" if obj_type == ALARM_AREA else "zone",
|
||||
"area" if obj_type == "alarm_areas" else "zone",
|
||||
)
|
||||
|
||||
return cast(T, data)
|
||||
return data
|
||||
|
||||
@@ -72,7 +72,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@property
|
||||
def device_status(self) -> int:
|
||||
"""Return current device status."""
|
||||
return cast("int", self.coordinator.data[COVER][self._device.index].status)
|
||||
return self.coordinator.data[COVER][self._device.index].status
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
@@ -86,7 +86,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@property
|
||||
def is_closing(self) -> bool:
|
||||
"""Return if the cover is closing."""
|
||||
return bool(self._current_action("closing"))
|
||||
return self._current_action("closing")
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool:
|
||||
|
||||
@@ -68,4 +68,4 @@ class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if light is on."""
|
||||
return bool(self.coordinator.data[LIGHT][self._device.index].status == STATE_ON)
|
||||
return self.coordinator.data[LIGHT][self._device.index].status == STATE_ON
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.0"]
|
||||
"requirements": ["aiocomelit==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Final, cast
|
||||
from typing import Final, cast
|
||||
|
||||
from aiocomelit.api import ComelitSerialBridgeObject, ComelitVedoZoneObject
|
||||
from aiocomelit.const import ALARM_ZONE, OTHER, AlarmZoneState
|
||||
from aiocomelit.const import BRIDGE, OTHER, AlarmZoneState
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.const import CONF_TYPE, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -52,20 +52,23 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Comelit sensors."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
is_bridge = isinstance(coordinator, ComelitSerialBridge)
|
||||
if config_entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
await async_setup_bridge_entry(hass, config_entry, async_add_entities)
|
||||
else:
|
||||
await async_setup_vedo_entry(hass, config_entry, async_add_entities)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if is_bridge:
|
||||
assert isinstance(coordinator, ComelitSerialBridge)
|
||||
else:
|
||||
assert isinstance(coordinator, ComelitVedoSystem)
|
||||
|
||||
def _add_new_bridge_entities(
|
||||
new_devices: list[ObjectClassType], dev_type: str
|
||||
) -> None:
|
||||
async def async_setup_bridge_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ComelitConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Comelit Bridge sensors."""
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
assert isinstance(coordinator, ComelitSerialBridge)
|
||||
entities = [
|
||||
ComelitBridgeSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
@@ -77,32 +80,36 @@ async def async_setup_entry(
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
def _add_new_vedo_entities(
|
||||
new_devices: list[ObjectClassType], dev_type: str
|
||||
) -> None:
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, OTHER)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_vedo_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ComelitConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Comelit VEDO sensors."""
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_VEDO_TYPES
|
||||
for device in coordinator.data[dev_type].values()
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
# Bridge native sensors
|
||||
if is_bridge:
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_bridge_entities, OTHER)
|
||||
)
|
||||
|
||||
# Alarm sensors (both via Bridge or VedoSystem)
|
||||
if coordinator.vedo_pin:
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_vedo_entities, ALARM_ZONE)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
)
|
||||
|
||||
|
||||
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):
|
||||
@@ -134,16 +141,14 @@ class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):
|
||||
)
|
||||
|
||||
|
||||
class ComelitVedoSensorEntity(
|
||||
CoordinatorEntity[ComelitVedoSystem | ComelitSerialBridge], SensorEntity
|
||||
):
|
||||
class ComelitVedoSensorEntity(CoordinatorEntity[ComelitVedoSystem], SensorEntity):
|
||||
"""Sensor device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ComelitVedoSystem | ComelitSerialBridge,
|
||||
coordinator: ComelitVedoSystem,
|
||||
zone: ComelitVedoZoneObject,
|
||||
config_entry_entry_id: str,
|
||||
description: SensorEntityDescription,
|
||||
@@ -161,9 +166,7 @@ class ComelitVedoSensorEntity(
|
||||
@property
|
||||
def _zone_object(self) -> ComelitVedoZoneObject:
|
||||
"""Zone object."""
|
||||
return cast(
|
||||
ComelitVedoZoneObject, self.coordinator.data[ALARM_ZONE][self._zone_index]
|
||||
)
|
||||
return self.coordinator.data["alarm_zones"][self._zone_index]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
|
||||
"invalid_vedo_auth": "The provided VEDO PIN is incorrect or VEDO alarm is not enabled on this device.",
|
||||
"invalid_vedo_pin": "The provided VEDO PIN is invalid. It must be a 4-10 digit number.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
@@ -15,34 +13,28 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
|
||||
"invalid_vedo_auth": "[%key:component::comelit::config::abort::invalid_vedo_auth%]",
|
||||
"invalid_vedo_pin": "[%key:component::comelit::config::abort::invalid_vedo_pin%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"flow_title": "{host}",
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"pin": "[%key:common::config_flow::data::pin%]",
|
||||
"vedo_pin": "[%key:component::comelit::config::step::user::data::vedo_pin%]"
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
},
|
||||
"data_description": {
|
||||
"pin": "The PIN of your Comelit device.",
|
||||
"vedo_pin": "[%key:component::comelit::config::step::user::data_description::vedo_pin%]"
|
||||
"pin": "The PIN of your Comelit device."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"pin": "[%key:common::config_flow::data::pin%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"vedo_pin": "[%key:component::comelit::config::step::user::data::vedo_pin%]"
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::comelit::config::step::user::data_description::host%]",
|
||||
"pin": "[%key:component::comelit::config::step::reauth_confirm::data_description::pin%]",
|
||||
"port": "[%key:component::comelit::config::step::user::data_description::port%]",
|
||||
"vedo_pin": "[%key:component::comelit::config::step::user::data_description::vedo_pin%]"
|
||||
"port": "[%key:component::comelit::config::step::user::data_description::port%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@@ -50,15 +42,13 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"pin": "[%key:common::config_flow::data::pin%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"type": "Device type",
|
||||
"vedo_pin": "VEDO alarm PIN (optional)"
|
||||
"type": "Device type"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Comelit device.",
|
||||
"pin": "[%key:component::comelit::config::step::reauth_confirm::data_description::pin%]",
|
||||
"port": "The port of your Comelit device.",
|
||||
"type": "The type of your Comelit device.",
|
||||
"vedo_pin": "Optional PIN for VEDO alarm system on Serial Bridge devices. Leave empty if you don't have VEDO alarm enabled."
|
||||
"type": "The type of your Comelit device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if switch is on."""
|
||||
return bool(
|
||||
return (
|
||||
self.coordinator.data[self._device.type][self._device.index].status
|
||||
== STATE_ON
|
||||
)
|
||||
|
||||
@@ -66,7 +66,6 @@ async def async_setup_entry(
|
||||
name="light",
|
||||
update_method=async_update_data_non_dimmer,
|
||||
update_interval=timedelta(seconds=runtime_data.scan_interval),
|
||||
config_entry=entry,
|
||||
)
|
||||
dimmer_coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]](
|
||||
hass,
|
||||
@@ -74,7 +73,6 @@ async def async_setup_entry(
|
||||
name="light",
|
||||
update_method=async_update_data_dimmer,
|
||||
update_interval=timedelta(seconds=runtime_data.scan_interval),
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
|
||||
@@ -110,7 +110,6 @@ async def async_setup_entry(
|
||||
name="room",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=scan_interval),
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.1.6"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2025.12.2"]
|
||||
}
|
||||
|
||||
@@ -14,12 +14,7 @@ from .const import DOMAIN
|
||||
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
|
||||
from .helpers import cookidoo_from_config_entry
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.CALENDAR,
|
||||
Platform.SENSOR,
|
||||
Platform.TODO,
|
||||
]
|
||||
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.TODO]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
"""Calendar platform for the Cookidoo integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
|
||||
from cookidoo_api import CookidooAuthException, CookidooException
|
||||
from cookidoo_api.types import CookidooCalendarDayRecipe
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
|
||||
from .entity import CookidooBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: CookidooConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the calendar platform for entity."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities([CookidooCalendarEntity(coordinator)])
|
||||
|
||||
|
||||
def recipe_to_event(day_date: date, recipe: CookidooCalendarDayRecipe) -> CalendarEvent:
|
||||
"""Convert a Cookidoo recipe to a CalendarEvent."""
|
||||
return CalendarEvent(
|
||||
start=day_date,
|
||||
end=day_date + timedelta(days=1), # All-day event
|
||||
summary=recipe.name,
|
||||
description=f"Total Time: {recipe.total_time}",
|
||||
)
|
||||
|
||||
|
||||
class CookidooCalendarEntity(CookidooBaseEntity, CalendarEntity):
|
||||
"""A calendar entity."""
|
||||
|
||||
_attr_translation_key = "meal_plan"
|
||||
|
||||
def __init__(self, coordinator: CookidooDataUpdateCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
assert coordinator.config_entry.unique_id
|
||||
self._attr_unique_id = coordinator.config_entry.unique_id
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
if not self.coordinator.data.week_plan:
|
||||
return None
|
||||
|
||||
today = date.today()
|
||||
for day_data in self.coordinator.data.week_plan:
|
||||
day_date = date.fromisoformat(day_data.id)
|
||||
if day_date >= today and day_data.recipes:
|
||||
recipe = day_data.recipes[0]
|
||||
return recipe_to_event(day_date, recipe)
|
||||
return None
|
||||
|
||||
async def _fetch_week_plan(self, week_day: date) -> list:
|
||||
"""Fetch a single Cookidoo week plan, retrying once on auth failure."""
|
||||
try:
|
||||
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
|
||||
week_day
|
||||
)
|
||||
except CookidooAuthException:
|
||||
await self.coordinator.cookidoo.refresh_token()
|
||||
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
|
||||
week_day
|
||||
)
|
||||
except CookidooException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="calendar_fetch_failed",
|
||||
) from e
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
events: list[CalendarEvent] = []
|
||||
current_day = start_date.date()
|
||||
while current_day <= end_date.date():
|
||||
week_plan = await self._fetch_week_plan(current_day)
|
||||
for day_data in week_plan:
|
||||
day_date = date.fromisoformat(day_data.id)
|
||||
if start_date.date() <= day_date <= end_date.date():
|
||||
events.extend(
|
||||
recipe_to_event(day_date, recipe) for recipe in day_data.recipes
|
||||
)
|
||||
current_day += timedelta(days=7) # Move to the next week
|
||||
return events
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from cookidoo_api import (
|
||||
@@ -16,7 +16,6 @@ from cookidoo_api import (
|
||||
CookidooSubscription,
|
||||
CookidooUserInfo,
|
||||
)
|
||||
from cookidoo_api.types import CookidooCalendarDay
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL
|
||||
@@ -38,7 +37,6 @@ class CookidooData:
|
||||
ingredient_items: list[CookidooIngredientItem]
|
||||
additional_items: list[CookidooAdditionalItem]
|
||||
subscription: CookidooSubscription | None
|
||||
week_plan: list[CookidooCalendarDay]
|
||||
|
||||
|
||||
class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
@@ -83,7 +81,6 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
ingredient_items = await self.cookidoo.get_ingredient_items()
|
||||
additional_items = await self.cookidoo.get_additional_items()
|
||||
subscription = await self.cookidoo.get_active_subscription()
|
||||
week_plan = await self.cookidoo.get_recipes_in_calendar_week(date.today())
|
||||
except CookidooAuthException:
|
||||
try:
|
||||
await self.cookidoo.refresh_token()
|
||||
@@ -109,5 +106,4 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
ingredient_items=ingredient_items,
|
||||
additional_items=additional_items,
|
||||
subscription=subscription,
|
||||
week_plan=week_plan,
|
||||
)
|
||||
|
||||
@@ -54,11 +54,6 @@
|
||||
"name": "Clear shopping list and additional purchases"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"meal_plan": {
|
||||
"name": "Meal plan"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"expires": {
|
||||
"name": "Subscription expiration date"
|
||||
@@ -85,9 +80,6 @@
|
||||
"button_clear_todo_failed": {
|
||||
"message": "Failed to clear all items from the Cookidoo shopping list"
|
||||
},
|
||||
"calendar_fetch_failed": {
|
||||
"message": "Failed to fetch Cookidoo meal plan"
|
||||
},
|
||||
"setup_authentication_exception": {
|
||||
"message": "Authentication failed for {email}, check your email and password"
|
||||
},
|
||||
|
||||
@@ -169,7 +169,6 @@ FRIENDS_OF_HUE_SWITCH = {
|
||||
}
|
||||
|
||||
RODRET_REMOTE_MODEL = "RODRET Dimmer"
|
||||
RODRET_REMOTE_MODEL_2 = "RODRET wireless dimmer"
|
||||
RODRET_REMOTE = {
|
||||
(CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
|
||||
(CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
|
||||
@@ -625,7 +624,6 @@ REMOTES = {
|
||||
HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE,
|
||||
FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH,
|
||||
RODRET_REMOTE_MODEL: RODRET_REMOTE,
|
||||
RODRET_REMOTE_MODEL_2: RODRET_REMOTE,
|
||||
SOMRIG_REMOTE_MODEL: SOMRIG_REMOTE,
|
||||
STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE,
|
||||
SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER,
|
||||
|
||||
@@ -28,11 +28,10 @@ async def async_setup_entry(
|
||||
DemoHumidifier(
|
||||
name="Humidifier",
|
||||
mode=None,
|
||||
target_humidity=65,
|
||||
target_humidity=68,
|
||||
current_humidity=45,
|
||||
action=HumidifierAction.HUMIDIFYING,
|
||||
device_class=HumidifierDeviceClass.HUMIDIFIER,
|
||||
target_humidity_step=5,
|
||||
),
|
||||
DemoHumidifier(
|
||||
name="Dehumidifier",
|
||||
@@ -67,7 +66,6 @@ class DemoHumidifier(HumidifierEntity):
|
||||
is_on: bool = True,
|
||||
action: HumidifierAction | None = None,
|
||||
device_class: HumidifierDeviceClass | None = None,
|
||||
target_humidity_step: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the humidifier device."""
|
||||
self._attr_name = name
|
||||
@@ -81,7 +79,6 @@ class DemoHumidifier(HumidifierEntity):
|
||||
self._attr_mode = mode
|
||||
self._attr_available_modes = available_modes
|
||||
self._attr_device_class = device_class
|
||||
self._attr_target_humidity_step = target_humidity_step
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.46.2", "getmac==0.9.5"],
|
||||
"requirements": ["async-upnp-client==0.46.1", "getmac==0.9.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["async-upnp-client==0.46.2"],
|
||||
"requirements": ["async-upnp-client==0.46.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==4.0.0"]
|
||||
"requirements": ["aiodns==3.6.1"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Duck DNS integration."""
|
||||
"""Integrate with DuckDNS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,16 +8,25 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import ATTR_CONFIG_ENTRY
|
||||
from .coordinator import DuckDnsConfigEntry, DuckDnsUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
from .helpers import update_duckdns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_TXT = "txt"
|
||||
|
||||
DOMAIN = "duckdns"
|
||||
|
||||
SERVICE_SET_TXT = "set_txt"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -31,11 +40,27 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_TXT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_CONFIG_ENTRY): ConfigEntrySelector(
|
||||
{
|
||||
"integration": DOMAIN,
|
||||
}
|
||||
),
|
||||
vol.Optional(ATTR_TXT): vol.Any(None, cv.string),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Initialize the DuckDNS component."""
|
||||
|
||||
async_setup_services(hass)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TXT,
|
||||
update_domain_service,
|
||||
schema=SERVICE_TXT_SCHEMA,
|
||||
)
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
@@ -62,6 +87,49 @@ async def async_setup_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> b
|
||||
return True
|
||||
|
||||
|
||||
def get_config_entry(
|
||||
hass: HomeAssistant, entry_id: str | None = None
|
||||
) -> DuckDnsConfigEntry:
|
||||
"""Return config entry or raise if not found or not loaded."""
|
||||
|
||||
if entry_id is None:
|
||||
if not (config_entries := hass.config_entries.async_entries(DOMAIN)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
|
||||
if len(config_entries) != 1:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_selected",
|
||||
)
|
||||
return config_entries[0]
|
||||
|
||||
if not (entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
async def update_domain_service(call: ServiceCall) -> None:
|
||||
"""Update the DuckDNS entry."""
|
||||
|
||||
entry = get_config_entry(call.hass, call.data.get(ATTR_CONFIG_ENTRY))
|
||||
|
||||
session = async_get_clientsession(call.hass)
|
||||
|
||||
await update_duckdns(
|
||||
session,
|
||||
entry.data[CONF_DOMAIN],
|
||||
entry.data[CONF_ACCESS_TOKEN],
|
||||
txt=call.data.get(ATTR_TXT),
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
@@ -5,5 +5,3 @@ from typing import Final
|
||||
DOMAIN = "duckdns"
|
||||
|
||||
ATTR_CONFIG_ENTRY: Final = "config_entry_id"
|
||||
ATTR_TXT: Final = "txt"
|
||||
SERVICE_SET_TXT = "set_txt"
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/duckdns",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Actions for Duck DNS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY, ATTR_TXT, DOMAIN, SERVICE_SET_TXT
|
||||
from .coordinator import DuckDnsConfigEntry
|
||||
from .helpers import update_duckdns
|
||||
|
||||
SERVICE_TXT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(ATTR_TXT): vol.Any(None, cv.string),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for Habitica integration."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TXT,
|
||||
update_domain_service,
|
||||
schema=SERVICE_TXT_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
def get_config_entry(
|
||||
hass: HomeAssistant, entry_id: str | None = None
|
||||
) -> DuckDnsConfigEntry:
|
||||
"""Return config entry or raise if not found or not loaded."""
|
||||
|
||||
if entry_id is None:
|
||||
if len(entries := hass.config_entries.async_entries(DOMAIN)) != 1:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_selected",
|
||||
)
|
||||
return entries[0]
|
||||
if not (entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
async def update_domain_service(call: ServiceCall) -> None:
|
||||
"""Update the DuckDNS entry."""
|
||||
|
||||
entry = get_config_entry(call.hass, call.data.get(ATTR_CONFIG_ENTRY))
|
||||
|
||||
session = async_get_clientsession(call.hass)
|
||||
|
||||
try:
|
||||
if not await update_duckdns(
|
||||
session,
|
||||
entry.data[CONF_DOMAIN],
|
||||
entry.data[CONF_ACCESS_TOKEN],
|
||||
txt=call.data.get(ATTR_TXT),
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={
|
||||
CONF_DOMAIN: entry.data[CONF_DOMAIN],
|
||||
},
|
||||
)
|
||||
except ClientError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={
|
||||
CONF_DOMAIN: entry.data[CONF_DOMAIN],
|
||||
},
|
||||
) from e
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["easyenergy==2.2.0"],
|
||||
"requirements": ["easyenergy==2.1.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.1"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from aioecowitt import EcoWittSensor, EcoWittSensorTypes
|
||||
@@ -40,9 +39,6 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
from . import EcowittConfigEntry
|
||||
from .entity import EcowittEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_METRIC: Final = (
|
||||
EcoWittSensorTypes.TEMPERATURE_C,
|
||||
EcoWittSensorTypes.RAIN_COUNT_MM,
|
||||
@@ -61,40 +57,6 @@ _IMPERIAL: Final = (
|
||||
)
|
||||
|
||||
|
||||
_RAIN_COUNT_SENSORS_STATE_CLASS_MAPPING: Final = {
|
||||
"eventrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"hourlyrainin": None,
|
||||
"totalrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"dailyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"weeklyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"monthlyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"yearlyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrainin": None,
|
||||
"eventrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"hourlyrainmm": None,
|
||||
"totalrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"dailyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"weeklyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"monthlyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"yearlyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrainmm": None,
|
||||
"erain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"hrain_piezo": None,
|
||||
"drain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"wrain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"mrain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"yrain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrain_piezo": None,
|
||||
"erain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"hrain_piezomm": None,
|
||||
"drain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"wrain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"mrain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"yrain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrain_piezomm": None,
|
||||
}
|
||||
|
||||
|
||||
ECOWITT_SENSORS_MAPPING: Final = {
|
||||
EcoWittSensorTypes.HUMIDITY: SensorEntityDescription(
|
||||
key="HUMIDITY",
|
||||
@@ -323,15 +285,15 @@ async def async_setup_entry(
|
||||
name=sensor.name,
|
||||
)
|
||||
|
||||
if sensor.stype in (
|
||||
EcoWittSensorTypes.RAIN_COUNT_INCHES,
|
||||
EcoWittSensorTypes.RAIN_COUNT_MM,
|
||||
# Only total rain needs state class for long-term statistics
|
||||
if sensor.key in (
|
||||
"totalrainin",
|
||||
"totalrainmm",
|
||||
):
|
||||
if sensor.key not in _RAIN_COUNT_SENSORS_STATE_CLASS_MAPPING:
|
||||
_LOGGER.warning("Unknown rain count sensor: %s", sensor.key)
|
||||
return
|
||||
state_class = _RAIN_COUNT_SENSORS_STATE_CLASS_MAPPING[sensor.key]
|
||||
description = dataclasses.replace(description, state_class=state_class)
|
||||
description = dataclasses.replace(
|
||||
description,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
|
||||
async_add_entities([EcowittSensorEntity(sensor, description)])
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
|
||||
name=device.name,
|
||||
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
|
||||
manufacturer="EHEIM",
|
||||
model=device.model_name,
|
||||
model=device.device_type.model_name,
|
||||
identifiers={(DOMAIN, device.mac_address)},
|
||||
suggested_area=device.aquarium_name,
|
||||
sw_version=device.sw_version,
|
||||
@@ -59,9 +59,9 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
|
||||
def exception_handler[_EntityT: EheimDigitalEntity[EheimDigitalDevice], **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate eheimdigital calls to handle exceptions.
|
||||
"""Decorate AirGradient calls to handle exceptions.
|
||||
|
||||
A decorator that wraps the passed in function, catches eheimdigital errors.
|
||||
A decorator that wraps the passed in function, catches AirGradient errors.
|
||||
"""
|
||||
|
||||
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["eheimdigital==1.5.0"],
|
||||
"requirements": ["eheimdigital==1.4.0"],
|
||||
"zeroconf": [
|
||||
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
from eheimdigital.heater import EheimDigitalHeater
|
||||
from eheimdigital.types import HeaterUnit
|
||||
|
||||
@@ -22,7 +21,6 @@ from homeassistant.const import (
|
||||
PRECISION_WHOLE,
|
||||
EntityCategory,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -44,34 +42,6 @@ class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
|
||||
uom_fn: Callable[[_DeviceT], str] | None = None
|
||||
|
||||
|
||||
FILTER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalFilter], ...] = (
|
||||
EheimDigitalNumberDescription[EheimDigitalFilter](
|
||||
key="high_pulse_time",
|
||||
translation_key="high_pulse_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=PRECISION_WHOLE,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_min_value=5,
|
||||
native_max_value=200000,
|
||||
value_fn=lambda device: device.high_pulse_time,
|
||||
set_value_fn=lambda device, value: device.set_high_pulse_time(int(value)),
|
||||
),
|
||||
EheimDigitalNumberDescription[EheimDigitalFilter](
|
||||
key="low_pulse_time",
|
||||
translation_key="low_pulse_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=PRECISION_WHOLE,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_min_value=5,
|
||||
native_max_value=200000,
|
||||
value_fn=lambda device: device.low_pulse_time,
|
||||
set_value_fn=lambda device, value: device.set_low_pulse_time(int(value)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalNumberDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
@@ -175,13 +145,6 @@ async def async_setup_entry(
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalFilter):
|
||||
entities.extend(
|
||||
EheimDigitalNumber[EheimDigitalFilter](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in FILTER_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalHeater):
|
||||
entities.extend(
|
||||
EheimDigitalNumber[EheimDigitalHeater](
|
||||
|
||||
@@ -2,19 +2,13 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
from eheimdigital.types import (
|
||||
FilterMode,
|
||||
FilterModeProf,
|
||||
UnitOfMeasurement as EheimDigitalUnitOfMeasurement,
|
||||
)
|
||||
from eheimdigital.types import FilterMode
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory, UnitOfFrequency, UnitOfVolumeFlowRate
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -30,109 +24,8 @@ class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
|
||||
):
|
||||
"""Class describing EHEIM Digital select entities."""
|
||||
|
||||
options_fn: Callable[[_DeviceT], list[str]] | None = None
|
||||
use_api_unit: Literal[True] | None = None
|
||||
value_fn: Callable[[_DeviceT], str | None]
|
||||
set_value_fn: Callable[[_DeviceT, str], Awaitable[None] | None]
|
||||
|
||||
|
||||
FILTER_DESCRIPTIONS: tuple[EheimDigitalSelectDescription[EheimDigitalFilter], ...] = (
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="filter_mode",
|
||||
translation_key="filter_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=[item.lower() for item in FilterModeProf._member_names_],
|
||||
value_fn=lambda device: device.filter_mode.name.lower(),
|
||||
set_value_fn=lambda device, value: device.set_filter_mode(
|
||||
FilterModeProf[value.upper()]
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="manual_speed",
|
||||
translation_key="manual_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_manual_values],
|
||||
value_fn=lambda device: str(device.manual_speed),
|
||||
set_value_fn=lambda device, value: device.set_manual_speed(float(value)),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="const_flow_speed",
|
||||
translation_key="const_flow_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(device.filter_const_flow_values[device.const_flow]),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_const_flow(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="day_speed",
|
||||
translation_key="day_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(device.filter_const_flow_values[device.day_speed]),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_day_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="night_speed",
|
||||
translation_key="night_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(
|
||||
device.filter_const_flow_values[device.night_speed]
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_night_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="high_pulse_speed",
|
||||
translation_key="high_pulse_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(
|
||||
device.filter_const_flow_values[device.high_pulse_speed]
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_high_pulse_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="low_pulse_speed",
|
||||
translation_key="low_pulse_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(
|
||||
device.filter_const_flow_values[device.low_pulse_speed]
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_low_pulse_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -141,7 +34,11 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalSelectDescription[EheimDigitalClassicVario](
|
||||
key="filter_mode",
|
||||
translation_key="filter_mode",
|
||||
value_fn=lambda device: device.filter_mode.name.lower(),
|
||||
value_fn=(
|
||||
lambda device: device.filter_mode.name.lower()
|
||||
if device.filter_mode is not None
|
||||
else None
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
|
||||
),
|
||||
@@ -171,11 +68,6 @@ async def async_setup_entry(
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalFilter):
|
||||
entities.extend(
|
||||
EheimDigitalFilterSelect(coordinator, device, description)
|
||||
for description in FILTER_DESCRIPTIONS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -190,8 +82,6 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT]
|
||||
|
||||
_attr_options: list[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
@@ -201,49 +91,13 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||
"""Initialize an EHEIM Digital select entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
if description.options_fn is not None:
|
||||
self._attr_options = description.options_fn(device)
|
||||
elif description.options is not None:
|
||||
self._attr_options = description.options
|
||||
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
if await_return := self.entity_description.set_value_fn(self._device, option):
|
||||
return await await_return
|
||||
return None
|
||||
return await self.entity_description.set_value_fn(self._device, option)
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
self._attr_current_option = self.entity_description.value_fn(self._device)
|
||||
|
||||
|
||||
class EheimDigitalFilterSelect(EheimDigitalSelect[EheimDigitalFilter]):
|
||||
"""Represent an EHEIM Digital Filter select entity."""
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[EheimDigitalFilter]
|
||||
_attr_native_unit_of_measurement: str | None
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
if (
|
||||
self.entity_description.options is None
|
||||
and self.entity_description.options_fn is not None
|
||||
):
|
||||
self._attr_options = self.entity_description.options_fn(self._device)
|
||||
if self.entity_description.use_api_unit:
|
||||
if (
|
||||
self.entity_description.unit_of_measurement
|
||||
== UnitOfVolumeFlowRate.LITERS_PER_HOUR
|
||||
and self._device.usrdta["unit"]
|
||||
== int(EheimDigitalUnitOfMeasurement.US_CUSTOMARY)
|
||||
):
|
||||
self._attr_native_unit_of_measurement = (
|
||||
UnitOfVolumeFlowRate.GALLONS_PER_HOUR
|
||||
)
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = (
|
||||
self.entity_description.unit_of_measurement
|
||||
)
|
||||
super()._async_update_attrs()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user