mirror of
https://github.com/home-assistant/core.git
synced 2026-01-11 01:57:16 +01:00
Compare commits
28 Commits
tibber_bin
...
2026.1.0b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ea7ecbd6 | ||
|
|
d58d08c350 | ||
|
|
65a259b9df | ||
|
|
cbfbfbee13 | ||
|
|
e503b37ddc | ||
|
|
217eef39f3 | ||
|
|
dcdbce9b21 | ||
|
|
71db8fe185 | ||
|
|
9b96cb66d5 | ||
|
|
78bccbbbc2 | ||
|
|
b0a8f9575c | ||
|
|
61104a9970 | ||
|
|
8d13dbdd0c | ||
|
|
9afb41004e | ||
|
|
cdd542f6e6 | ||
|
|
f520686002 | ||
|
|
e4d09bb615 | ||
|
|
10f6ccf6cc | ||
|
|
d9fa67b16f | ||
|
|
cf228ae02b | ||
|
|
cb4d62ab9a | ||
|
|
d2f75aec04 | ||
|
|
a609fbc07b | ||
|
|
1b9c7ae0ac | ||
|
|
492f2117fb | ||
|
|
2346f83635 | ||
|
|
8925bfb182 | ||
|
|
8f2b1f0eff |
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
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1170,8 +1170,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/open_router/ @joostlek
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openevse/ @c00w
|
||||
/tests/components/openevse/ @c00w
|
||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||
/tests/components/openexchangerates/ @MartinHjelmare
|
||||
/homeassistant/components/opengarage/ @danielhiversen
|
||||
|
||||
@@ -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,89 +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(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyairobotrest==0.2.0"]
|
||||
"requirements": ["pyairobotrest==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -86,9 +86,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"]
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"scene",
|
||||
"siren",
|
||||
"switch",
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
"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==3.1.6"],
|
||||
"requirements": ["python-bsblan==3.1.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -7,13 +7,11 @@ 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 homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -31,32 +29,32 @@ ATTR_FRIDAY_SLOTS = "friday_slots"
|
||||
ATTR_SATURDAY_SLOTS = "saturday_slots"
|
||||
ATTR_SUNDAY_SLOTS = "sunday_slots"
|
||||
|
||||
# Service names
|
||||
# Service name
|
||||
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,
|
||||
}
|
||||
)
|
||||
def _parse_time_value(value: time | str) -> time:
|
||||
"""Parse a time value from either a time object or string.
|
||||
|
||||
Raises ServiceValidationError if the format is invalid.
|
||||
"""
|
||||
if isinstance(value, time):
|
||||
return value
|
||||
|
||||
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]),
|
||||
}
|
||||
)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parts = value.split(":")
|
||||
return time(int(parts[0]), int(parts[1]))
|
||||
except (ValueError, IndexError):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_time_format",
|
||||
) from None
|
||||
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_time_format",
|
||||
)
|
||||
|
||||
|
||||
def _convert_time_slots_to_day_schedule(
|
||||
@@ -64,8 +62,8 @@ def _convert_time_slots_to_day_schedule(
|
||||
) -> 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)}]
|
||||
Example: [{"start_time": "06:00", "end_time": "08:00"},
|
||||
{"start_time": "17:00", "end_time": "21:00"}]
|
||||
becomes: DaySchedule with two TimeSlot objects
|
||||
|
||||
None returns None (don't modify this day).
|
||||
@@ -79,27 +77,31 @@ def _convert_time_slots_to_day_schedule(
|
||||
|
||||
time_slots = []
|
||||
for slot in slots:
|
||||
start_time = slot["start_time"]
|
||||
end_time = slot["end_time"]
|
||||
start = slot.get("start_time")
|
||||
end = slot.get("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"),
|
||||
},
|
||||
if start and end:
|
||||
start_time = _parse_time_value(start)
|
||||
end_time = _parse_time_value(end)
|
||||
|
||||
# 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"),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -205,74 +207,6 @@ async def set_hot_water_schedule(service_call: ServiceCall) -> None:
|
||||
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."""
|
||||
@@ -280,12 +214,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
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,12 +1,3 @@
|
||||
sync_time:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
example: "abc123device456"
|
||||
selector:
|
||||
device:
|
||||
integration: bsblan
|
||||
|
||||
set_hot_water_schedule:
|
||||
fields:
|
||||
device_id:
|
||||
|
||||
@@ -79,6 +79,9 @@
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID: {device_id}"
|
||||
},
|
||||
"invalid_time_format": {
|
||||
"message": "Invalid time format provided"
|
||||
},
|
||||
"no_config_entry_for_device": {
|
||||
"message": "No configuration entry found for device: {device_id}"
|
||||
},
|
||||
@@ -105,9 +108,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": {
|
||||
@@ -148,16 +148,6 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.1"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2025.12.2"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Duck DNS integration."""
|
||||
"""Integrate with DuckDNS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/duckdns",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
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.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
|
||||
@@ -63,25 +62,9 @@ async def update_domain_service(call: ServiceCall) -> None:
|
||||
|
||||
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
|
||||
await update_duckdns(
|
||||
session,
|
||||
entry.data[CONF_DOMAIN],
|
||||
entry.data[CONF_ACCESS_TOKEN],
|
||||
txt=call.data.get(ATTR_TXT),
|
||||
)
|
||||
|
||||
@@ -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)])
|
||||
|
||||
|
||||
@@ -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." }
|
||||
]
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==43.10.1",
|
||||
"aioesphomeapi==43.9.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.4.0"
|
||||
],
|
||||
|
||||
@@ -19,9 +19,6 @@ from .coordinator import FeedReaderCoordinator
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
ATTR_CONTENT = "content"
|
||||
ATTR_DESCRIPTION = "description"
|
||||
ATTR_LINK = "link"
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: No custom actions are defined.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: missing test for uniqueness of feed URL.
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: missing data descriptions
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: No custom actions are defined.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: No custom actions are defined.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: No authentication support.
|
||||
test-coverage:
|
||||
status: done
|
||||
comment: Can use freezer for skipping time instead
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: No discovery support.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: No discovery support.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Each config entry, represents one service.
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: Matches no available event entity class.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: Only one entity per config entry.
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: done
|
||||
comment: Only one repair-issue for yaml-import defined.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Each config entry, represents one service.
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: todo
|
||||
comment: feedparser lib is not async.
|
||||
inject-websession:
|
||||
status: todo
|
||||
comment: feedparser lib doesn't take a session as argument.
|
||||
strict-typing:
|
||||
status: todo
|
||||
comment: feedparser lib is not fully typed.
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyfirefly==0.1.10"]
|
||||
"requirements": ["pyfirefly==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,6 @@ BACKEND_MODELS = ["s1", "speech-1.5", "speech-1.6"]
|
||||
SORT_BY_OPTIONS = ["task_count", "score", "created_at"]
|
||||
LATENCY_OPTIONS = ["normal", "balanced"]
|
||||
|
||||
SIGNUP_URL = "https://fish.audio/"
|
||||
SIGNUP_URL = "https://fish.audio/?fpr=homeassistant" # codespell:ignore fpr
|
||||
BILLING_URL = "https://fish.audio/app/billing/"
|
||||
API_KEYS_URL = "https://fish.audio/app/api-keys/"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from fressnapftracker import FressnapfTrackerError
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ColorMode,
|
||||
@@ -18,7 +16,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import FressnapfTrackerConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import FressnapfTrackerEntity
|
||||
from .services import handle_fressnapf_tracker_exception
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -64,18 +61,12 @@ class FressnapfTrackerLight(FressnapfTrackerEntity, LightEntity):
|
||||
self.raise_if_not_activatable()
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
|
||||
brightness = int((brightness / 255) * 100)
|
||||
try:
|
||||
await self.coordinator.client.set_led_brightness(brightness)
|
||||
except FressnapfTrackerError as e:
|
||||
handle_fressnapf_tracker_exception(e)
|
||||
await self.coordinator.client.set_led_brightness(brightness)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the device."""
|
||||
try:
|
||||
await self.coordinator.client.set_led_brightness(0)
|
||||
except FressnapfTrackerError as e:
|
||||
handle_fressnapf_tracker_exception(e)
|
||||
await self.coordinator.client.set_led_brightness(0)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def raise_if_not_activatable(self) -> None:
|
||||
|
||||
@@ -26,7 +26,7 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Services and service helpers for fressnapf_tracker."""
|
||||
|
||||
from fressnapftracker import FressnapfTrackerError, FressnapfTrackerInvalidTokenError
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def handle_fressnapf_tracker_exception(exception: FressnapfTrackerError):
|
||||
"""Handle the different FressnapfTracker errors."""
|
||||
if isinstance(exception, FressnapfTrackerInvalidTokenError):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
) from exception
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error_message": str(exception)},
|
||||
) from exception
|
||||
@@ -77,9 +77,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error": {
|
||||
"message": "An error occurred while communicating with the Fressnapf Tracker API: {error_message}"
|
||||
},
|
||||
"charging": {
|
||||
"message": "The flashlight cannot be activated while charging."
|
||||
},
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from fressnapftracker import FressnapfTrackerError
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
@@ -15,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import FressnapfTrackerConfigEntry
|
||||
from .entity import FressnapfTrackerEntity
|
||||
from .services import handle_fressnapf_tracker_exception
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -46,18 +43,12 @@ class FressnapfTrackerSwitch(FressnapfTrackerEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the device."""
|
||||
try:
|
||||
await self.coordinator.client.set_energy_saving(True)
|
||||
except FressnapfTrackerError as e:
|
||||
handle_fressnapf_tracker_exception(e)
|
||||
await self.coordinator.client.set_energy_saving(True)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the device."""
|
||||
try:
|
||||
await self.coordinator.client.set_energy_saving(False)
|
||||
except FressnapfTrackerError as e:
|
||||
handle_fressnapf_tracker_exception(e)
|
||||
await self.coordinator.client.set_energy_saving(False)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["google-genai==1.56.0"]
|
||||
"requirements": ["google-genai==1.38.0"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"bluetooth": [
|
||||
{
|
||||
"connectable": true,
|
||||
"service_data_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb",
|
||||
"service_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb"
|
||||
"service_data_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@flip-dots"],
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"trigger": "mdi:air-humidifier-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:air-humidifier"
|
||||
"trigger": "mdi:air-humidifier-on"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["librehardwaremonitor-api==1.8.4"]
|
||||
"requirements": ["librehardwaremonitor-api==1.6.0"]
|
||||
}
|
||||
|
||||
@@ -154,7 +154,6 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
(0x1209, 0x8027),
|
||||
(0x1209, 0x8028),
|
||||
(0x1209, 0x8029),
|
||||
(0x131A, 0x1000),
|
||||
}
|
||||
|
||||
SystemModeEnum = clusters.Thermostat.Enums.SystemModeEnum
|
||||
|
||||
@@ -19,12 +19,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
MieleAuxDataUpdateCoordinator,
|
||||
MieleConfigEntry,
|
||||
MieleDataUpdateCoordinator,
|
||||
MieleRuntimeData,
|
||||
)
|
||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
@@ -80,23 +75,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
# Setup MieleAPI and coordinator for data fetch
|
||||
_api = MieleAPI(auth)
|
||||
_coordinator = MieleDataUpdateCoordinator(hass, entry, _api)
|
||||
await _coordinator.async_config_entry_first_refresh()
|
||||
_aux_coordinator = MieleAuxDataUpdateCoordinator(hass, entry, _api)
|
||||
await _aux_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = MieleRuntimeData(_api, _coordinator, _aux_coordinator)
|
||||
api = MieleAPI(auth)
|
||||
coordinator = MieleDataUpdateCoordinator(hass, entry, api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
entry.runtime_data.api.listen_events(
|
||||
data_callback=_coordinator.callback_update_data,
|
||||
actions_callback=_coordinator.callback_update_actions,
|
||||
coordinator.api.listen_events(
|
||||
data_callback=coordinator.callback_update_data,
|
||||
actions_callback=coordinator.callback_update_actions,
|
||||
),
|
||||
"pymiele event listener",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
@@ -116,5 +107,5 @@ async def async_remove_config_entry_device(
|
||||
identifier
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
and identifier[1] in config_entry.runtime_data.coordinator.data.devices
|
||||
and identifier[1] in config_entry.runtime_data.data.devices
|
||||
)
|
||||
|
||||
@@ -264,7 +264,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the binary sensor platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -112,7 +112,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the button platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -138,7 +138,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the climate platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -9,13 +9,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pymiele import (
|
||||
MieleAction,
|
||||
MieleAPI,
|
||||
MieleDevice,
|
||||
MieleFillingLevel,
|
||||
MieleFillingLevels,
|
||||
)
|
||||
from pymiele import MieleAction, MieleAPI, MieleDevice
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -26,16 +20,7 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MieleRuntimeData:
|
||||
"""Runtime data for the Miele integration."""
|
||||
|
||||
api: MieleAPI
|
||||
coordinator: MieleDataUpdateCoordinator
|
||||
aux_coordinator: MieleAuxDataUpdateCoordinator
|
||||
|
||||
|
||||
type MieleConfigEntry = ConfigEntry[MieleRuntimeData]
|
||||
type MieleConfigEntry = ConfigEntry[MieleDataUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -46,15 +31,8 @@ class MieleCoordinatorData:
|
||||
actions: dict[str, MieleAction]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MieleAuxCoordinatorData:
|
||||
"""Data class for storing auxiliary coordinator data."""
|
||||
|
||||
filling_levels: dict[str, MieleFillingLevel]
|
||||
|
||||
|
||||
class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
"""Main coordinator for Miele data."""
|
||||
"""Coordinator for Miele data."""
|
||||
|
||||
config_entry: MieleConfigEntry
|
||||
new_device_callbacks: list[Callable[[dict[str, MieleDevice]], None]] = []
|
||||
@@ -88,7 +66,6 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
}
|
||||
self.devices = devices
|
||||
actions = {}
|
||||
|
||||
for device_id in devices:
|
||||
try:
|
||||
actions_json = await self.api.get_actions(device_id)
|
||||
@@ -122,7 +99,10 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
device_id: MieleDevice(device) for device_id, device in devices_json.items()
|
||||
}
|
||||
self.async_set_updated_data(
|
||||
MieleCoordinatorData(devices=devices, actions=self.data.actions)
|
||||
MieleCoordinatorData(
|
||||
devices=devices,
|
||||
actions=self.data.actions,
|
||||
)
|
||||
)
|
||||
|
||||
async def callback_update_actions(self, actions_json: dict[str, dict]) -> None:
|
||||
@@ -131,34 +111,8 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
device_id: MieleAction(action) for device_id, action in actions_json.items()
|
||||
}
|
||||
self.async_set_updated_data(
|
||||
MieleCoordinatorData(devices=self.data.devices, actions=actions)
|
||||
)
|
||||
|
||||
|
||||
class MieleAuxDataUpdateCoordinator(DataUpdateCoordinator[MieleAuxCoordinatorData]):
|
||||
"""Coordinator for Miele data for slowly polled endpoints."""
|
||||
|
||||
config_entry: MieleConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MieleConfigEntry,
|
||||
api: MieleAPI,
|
||||
) -> None:
|
||||
"""Initialize the Miele data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
self.api = api
|
||||
|
||||
async def _async_update_data(self) -> MieleAuxCoordinatorData:
|
||||
"""Fetch data from the Miele API."""
|
||||
filling_levels_json = await self.api.get_filling_levels()
|
||||
return MieleAuxCoordinatorData(
|
||||
filling_levels=MieleFillingLevels(filling_levels_json).filling_levels
|
||||
MieleCoordinatorData(
|
||||
devices=self.data.devices,
|
||||
actions=actions,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -38,19 +38,13 @@ async def async_get_config_entry_diagnostics(
|
||||
"devices": redact_identifiers(
|
||||
{
|
||||
device_id: device_data.raw
|
||||
for device_id, device_data in config_entry.runtime_data.coordinator.data.devices.items()
|
||||
}
|
||||
),
|
||||
"filling_levels": redact_identifiers(
|
||||
{
|
||||
device_id: filling_level_data.raw
|
||||
for device_id, filling_level_data in config_entry.runtime_data.aux_coordinator.data.filling_levels.items()
|
||||
for device_id, device_data in config_entry.runtime_data.data.devices.items()
|
||||
}
|
||||
),
|
||||
"actions": redact_identifiers(
|
||||
{
|
||||
device_id: action_data.raw
|
||||
for device_id, action_data in config_entry.runtime_data.coordinator.data.actions.items()
|
||||
for device_id, action_data in config_entry.runtime_data.data.actions.items()
|
||||
}
|
||||
),
|
||||
}
|
||||
@@ -74,19 +68,13 @@ async def async_get_device_diagnostics(
|
||||
"model_id": device.model_id,
|
||||
}
|
||||
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
aux_coordinator = config_entry.runtime_data.aux_coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
device_id = cast(str, device.serial_number)
|
||||
miele_data: dict[str, Any] = {
|
||||
"devices": {
|
||||
hash_identifier(device_id): coordinator.data.devices[device_id].raw
|
||||
},
|
||||
"filling_levels": {
|
||||
hash_identifier(device_id): aux_coordinator.data.filling_levels[
|
||||
device_id
|
||||
].raw
|
||||
},
|
||||
"actions": {
|
||||
hash_identifier(device_id): coordinator.data.actions[device_id].raw
|
||||
},
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
"""Entity base class for the Miele integration."""
|
||||
|
||||
from pymiele import MieleAction, MieleAPI, MieleDevice, MieleFillingLevel
|
||||
from pymiele import MieleAction, MieleAPI, MieleDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEVICE_TYPE_TAGS, DOMAIN, MANUFACTURER, MieleAppliance, StateStatus
|
||||
from .coordinator import MieleAuxDataUpdateCoordinator, MieleDataUpdateCoordinator
|
||||
from .coordinator import MieleDataUpdateCoordinator
|
||||
|
||||
|
||||
class MieleBaseEntity[
|
||||
_MieleCoordinatorT: MieleDataUpdateCoordinator | MieleAuxDataUpdateCoordinator
|
||||
](CoordinatorEntity[_MieleCoordinatorT]):
|
||||
class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
|
||||
"""Base class for Miele entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
@@ -24,7 +22,7 @@ class MieleBaseEntity[
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: _MieleCoordinatorT,
|
||||
coordinator: MieleDataUpdateCoordinator,
|
||||
device_id: str,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
@@ -32,26 +30,7 @@ class MieleBaseEntity[
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = MieleBaseEntity.get_unique_id(device_id, description)
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_id)})
|
||||
|
||||
@property
|
||||
def api(self) -> MieleAPI:
|
||||
"""Return the api object."""
|
||||
return self.coordinator.api
|
||||
|
||||
|
||||
class MieleEntity(MieleBaseEntity[MieleDataUpdateCoordinator]):
|
||||
"""Base class for Miele entities that use the main data coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MieleDataUpdateCoordinator,
|
||||
device_id: str,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, device_id, description)
|
||||
self._attr_unique_id = MieleEntity.get_unique_id(device_id, description)
|
||||
|
||||
device = self.device
|
||||
appliance_type = DEVICE_TYPE_TAGS.get(MieleAppliance(device.device_type))
|
||||
@@ -82,6 +61,11 @@ class MieleEntity(MieleBaseEntity[MieleDataUpdateCoordinator]):
|
||||
"""Return the actions object."""
|
||||
return self.coordinator.data.actions[self._device_id]
|
||||
|
||||
@property
|
||||
def api(self) -> MieleAPI:
|
||||
"""Return the api object."""
|
||||
return self.coordinator.api
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability of the entity."""
|
||||
@@ -91,12 +75,3 @@ class MieleEntity(MieleBaseEntity[MieleDataUpdateCoordinator]):
|
||||
and self._device_id in self.coordinator.data.devices
|
||||
and (self.device.state_status is not StateStatus.not_connected)
|
||||
)
|
||||
|
||||
|
||||
class MieleAuxEntity(MieleBaseEntity[MieleAuxDataUpdateCoordinator]):
|
||||
"""Base class for Miele entities that use the auxiliary data coordinator."""
|
||||
|
||||
@property
|
||||
def levels(self) -> MieleFillingLevel:
|
||||
"""Return the filling levels object."""
|
||||
return self.coordinator.data.filling_levels[self._device_id]
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the fan platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -71,9 +71,6 @@
|
||||
"plate_step_warming": "mdi:alpha-w-circle-outline"
|
||||
}
|
||||
},
|
||||
"power_disk_level": {
|
||||
"default": "mdi:car-coolant-level"
|
||||
},
|
||||
"program_id": {
|
||||
"default": "mdi:selection-ellipse-arrow-inside"
|
||||
},
|
||||
@@ -86,12 +83,6 @@
|
||||
"remaining_time": {
|
||||
"default": "mdi:clock-end"
|
||||
},
|
||||
"rinse_aid_level": {
|
||||
"default": "mdi:water-opacity"
|
||||
},
|
||||
"salt_level": {
|
||||
"default": "mdi:shaker-outline"
|
||||
},
|
||||
"spin_speed": {
|
||||
"default": "mdi:sync"
|
||||
},
|
||||
@@ -104,12 +95,6 @@
|
||||
"target_temperature": {
|
||||
"default": "mdi:thermometer-check"
|
||||
},
|
||||
"twin_dos_1_level": {
|
||||
"default": "mdi:car-coolant-level"
|
||||
},
|
||||
"twin_dos_2_level": {
|
||||
"default": "mdi:car-coolant-level"
|
||||
},
|
||||
"water_forecast": {
|
||||
"default": "mdi:water-outline"
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the light platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -71,7 +71,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the select platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from pymiele import MieleDevice, MieleFillingLevel, MieleTemperature
|
||||
from pymiele import MieleDevice, MieleTemperature
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
RestoreSensor,
|
||||
@@ -44,12 +44,8 @@ from .const import (
|
||||
StateProgramType,
|
||||
StateStatus,
|
||||
)
|
||||
from .coordinator import (
|
||||
MieleAuxDataUpdateCoordinator,
|
||||
MieleConfigEntry,
|
||||
MieleDataUpdateCoordinator,
|
||||
)
|
||||
from .entity import MieleAuxEntity, MieleEntity
|
||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -143,13 +139,10 @@ def _convert_finish_timestamp(
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MieleSensorDescription[T: (MieleDevice, MieleFillingLevel)](
|
||||
SensorEntityDescription
|
||||
):
|
||||
class MieleSensorDescription(SensorEntityDescription):
|
||||
"""Class describing Miele sensor entities."""
|
||||
|
||||
value_fn: Callable[[T], StateType | datetime]
|
||||
|
||||
value_fn: Callable[[MieleDevice], StateType | datetime]
|
||||
end_value_fn: Callable[[StateType | datetime], StateType | datetime] | None = None
|
||||
extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None
|
||||
zone: int | None = None
|
||||
@@ -157,14 +150,14 @@ class MieleSensorDescription[T: (MieleDevice, MieleFillingLevel)](
|
||||
|
||||
|
||||
@dataclass
|
||||
class MieleSensorDefinition[T: (MieleDevice, MieleFillingLevel)]:
|
||||
class MieleSensorDefinition:
|
||||
"""Class for defining sensor entities."""
|
||||
|
||||
types: tuple[MieleAppliance, ...]
|
||||
description: MieleSensorDescription[T]
|
||||
description: MieleSensorDescription
|
||||
|
||||
|
||||
SENSOR_TYPES: Final[tuple[MieleSensorDefinition[MieleDevice], ...]] = (
|
||||
SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
MieleSensorDefinition(
|
||||
types=(
|
||||
MieleAppliance.WASHING_MACHINE,
|
||||
@@ -696,59 +689,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition[MieleDevice], ...]] = (
|
||||
),
|
||||
)
|
||||
|
||||
POLLED_SENSOR_TYPES: Final[tuple[MieleSensorDefinition[MieleFillingLevel], ...]] = (
|
||||
MieleSensorDefinition(
|
||||
types=(MieleAppliance.WASHING_MACHINE,),
|
||||
description=MieleSensorDescription[MieleFillingLevel](
|
||||
key="twin_dos_1_level",
|
||||
translation_key="twin_dos_1_level",
|
||||
value_fn=lambda value: value.twin_dos_container_1_filling_level,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(MieleAppliance.WASHING_MACHINE,),
|
||||
description=MieleSensorDescription[MieleFillingLevel](
|
||||
key="twin_dos_2_level",
|
||||
translation_key="twin_dos_2_level",
|
||||
value_fn=lambda value: value.twin_dos_container_2_filling_level,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(MieleAppliance.DISHWASHER,),
|
||||
description=MieleSensorDescription[MieleFillingLevel](
|
||||
key="power_disk_level",
|
||||
translation_key="power_disk_level",
|
||||
value_fn=lambda value: None,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(MieleAppliance.DISHWASHER,),
|
||||
description=MieleSensorDescription[MieleFillingLevel](
|
||||
key="salt_level",
|
||||
translation_key="salt_level",
|
||||
value_fn=lambda value: value.salt_filling_level,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(MieleAppliance.DISHWASHER,),
|
||||
description=MieleSensorDescription[MieleFillingLevel](
|
||||
key="rinse_aid_level",
|
||||
translation_key="rinse_aid_level",
|
||||
value_fn=lambda value: value.rinse_aid_filling_level,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -756,14 +696,11 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
aux_coordinator = config_entry.runtime_data.aux_coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set() # device_id
|
||||
added_entities: set[str] = set() # unique_id
|
||||
|
||||
def _get_entity_class(
|
||||
definition: MieleSensorDefinition[MieleDevice],
|
||||
) -> type[MieleSensor]:
|
||||
def _get_entity_class(definition: MieleSensorDefinition) -> type[MieleSensor]:
|
||||
"""Get the entity class for the sensor."""
|
||||
return {
|
||||
"state_status": MieleStatusSensor,
|
||||
@@ -788,7 +725,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
def _is_sensor_enabled(
|
||||
definition: MieleSensorDefinition[MieleDevice],
|
||||
definition: MieleSensorDefinition,
|
||||
device: MieleDevice,
|
||||
unique_id: str,
|
||||
) -> bool:
|
||||
@@ -811,15 +748,6 @@ async def async_setup_entry(
|
||||
return False
|
||||
return True
|
||||
|
||||
def _enabled_aux_sensor(
|
||||
definition: MieleSensorDefinition[MieleFillingLevel], level: MieleFillingLevel
|
||||
) -> bool:
|
||||
"""Check if aux sensors are enabled."""
|
||||
return not (
|
||||
definition.description.value_fn is not None
|
||||
and definition.description.value_fn(level) is None
|
||||
)
|
||||
|
||||
def _async_add_devices() -> None:
|
||||
nonlocal added_devices, added_entities
|
||||
entities: list = []
|
||||
@@ -847,11 +775,7 @@ async def async_setup_entry(
|
||||
continue
|
||||
|
||||
# sensors is not enabled, skip
|
||||
if not _is_sensor_enabled(
|
||||
definition,
|
||||
device,
|
||||
unique_id,
|
||||
):
|
||||
if not _is_sensor_enabled(definition, device, unique_id):
|
||||
continue
|
||||
|
||||
added_entities.add(unique_id)
|
||||
@@ -863,15 +787,6 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_devices))
|
||||
_async_add_devices()
|
||||
|
||||
async_add_entities(
|
||||
MieleAuxSensor(aux_coordinator, device_id, definition.description)
|
||||
for device_id in aux_coordinator.data.filling_levels
|
||||
for definition in POLLED_SENSOR_TYPES
|
||||
if _enabled_aux_sensor(
|
||||
definition, aux_coordinator.data.filling_levels[device_id]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
APPLIANCE_ICONS = {
|
||||
MieleAppliance.WASHING_MACHINE: "mdi:washing-machine",
|
||||
@@ -970,32 +885,6 @@ class MieleRestorableSensor(MieleSensor, RestoreSensor):
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
class MieleAuxSensor(MieleAuxEntity, SensorEntity):
|
||||
"""Representation of a filling level Sensor."""
|
||||
|
||||
entity_description: MieleSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MieleAuxDataUpdateCoordinator,
|
||||
device_id: str,
|
||||
description: MieleSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, device_id, description)
|
||||
if description.unique_id_fn is not None:
|
||||
self._attr_unique_id = description.unique_id_fn(device_id, description)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the level sensor."""
|
||||
return (
|
||||
self.entity_description.value_fn(self.levels)
|
||||
if self.entity_description.value_fn is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
class MielePlateSensor(MieleSensor):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
|
||||
@@ -257,9 +257,6 @@
|
||||
"plate_step_warm": "Warming"
|
||||
}
|
||||
},
|
||||
"power_disk_level": {
|
||||
"name": "PowerDisk level"
|
||||
},
|
||||
"program_id": {
|
||||
"name": "Program",
|
||||
"state": {
|
||||
@@ -1041,12 +1038,6 @@
|
||||
"remaining_time": {
|
||||
"name": "Remaining time"
|
||||
},
|
||||
"rinse_aid_level": {
|
||||
"name": "Rinse aid level"
|
||||
},
|
||||
"salt_level": {
|
||||
"name": "Salt level"
|
||||
},
|
||||
"spin_speed": {
|
||||
"name": "Spin speed"
|
||||
},
|
||||
@@ -1089,12 +1080,6 @@
|
||||
"temperature_zone_3": {
|
||||
"name": "Temperature zone 3"
|
||||
},
|
||||
"twin_dos_1_level": {
|
||||
"name": "TwinDos 1 level"
|
||||
},
|
||||
"twin_dos_2_level": {
|
||||
"name": "TwinDos 2 level"
|
||||
},
|
||||
"water_consumption": {
|
||||
"name": "Water consumption"
|
||||
},
|
||||
|
||||
@@ -117,7 +117,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
added_devices: set[str] = set()
|
||||
|
||||
def _async_add_new_devices() -> None:
|
||||
|
||||
@@ -128,7 +128,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the vacuum platform."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MieleVacuum(coordinator, device_id, definition.description)
|
||||
|
||||
@@ -69,7 +69,7 @@ class RegistrationsView(HomeAssistantView):
|
||||
|
||||
webhook_id = secrets.token_hex()
|
||||
|
||||
if cloud.async_active_subscription(hass) and cloud.async_is_connected(hass):
|
||||
if cloud.async_active_subscription(hass):
|
||||
data[CONF_CLOUDHOOK_URL] = await async_create_cloud_hook(
|
||||
hass, webhook_id, None
|
||||
)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nextdns"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["nextdns==5.0.0"]
|
||||
"requirements": ["nextdns==4.1.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["nibe==2.21.0"]
|
||||
"requirements": ["nibe==2.20.0"]
|
||||
}
|
||||
|
||||
@@ -1,30 +1 @@
|
||||
"""The OpenEVSE integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import openevsewifi
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
|
||||
type OpenEVSEConfigEntry = ConfigEntry[openevsewifi.Charger]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) -> bool:
|
||||
"""Set up openevse from a config entry."""
|
||||
|
||||
entry.runtime_data = openevsewifi.Charger(entry.data[CONF_HOST])
|
||||
try:
|
||||
await hass.async_add_executor_job(entry.runtime_data.getStatus)
|
||||
except AttributeError as ex:
|
||||
raise ConfigEntryError("Unable to connect to charger") from ex
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, [Platform.SENSOR])
|
||||
"""The openevse component."""
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Config flow for OpenEVSE integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import openevsewifi
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""OpenEVSE config flow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def check_status(self, host: str) -> bool:
|
||||
"""Check if we can connect to the OpenEVSE charger."""
|
||||
|
||||
charger = openevsewifi.Charger(host)
|
||||
try:
|
||||
result = await self.hass.async_add_executor_job(charger.getStatus)
|
||||
except AttributeError:
|
||||
return False
|
||||
else:
|
||||
return result is not None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
if await self.check_status(user_input[CONF_HOST]):
|
||||
return self.async_create_entry(
|
||||
title=f"OpenEVSE {user_input[CONF_HOST]}",
|
||||
data=user_input,
|
||||
)
|
||||
errors = {CONF_HOST: "cannot_connect"}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, data: dict[str, str]) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
self._async_abort_entries_match({CONF_HOST: data[CONF_HOST]})
|
||||
|
||||
if not await self.check_status(data[CONF_HOST]):
|
||||
return self.async_abort(reason="unavailable_host")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"OpenEVSE {data[CONF_HOST]}",
|
||||
data=data,
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
"""Constants for the OpenEVSE integration."""
|
||||
|
||||
DOMAIN = "openevse"
|
||||
INTEGRATION_TITLE = "OpenEVSE"
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"domain": "openevse",
|
||||
"name": "OpenEVSE",
|
||||
"codeowners": ["@c00w"],
|
||||
"config_flow": true,
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/openevse",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openevsewifi"],
|
||||
"quality_scale": "legacy",
|
||||
|
||||
@@ -9,14 +9,12 @@ from requests import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MONITORED_VARIABLES,
|
||||
@@ -25,17 +23,10 @@ from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import ConfigEntry
|
||||
from .const import DOMAIN, INTEGRATION_TITLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
@@ -63,7 +54,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="rtc_temp",
|
||||
@@ -71,7 +61,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="usage_session",
|
||||
@@ -101,86 +90,33 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the openevse platform."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
"""Set up the OpenEVSE sensor."""
|
||||
host = config[CONF_HOST]
|
||||
monitored_variables = config[CONF_MONITORED_VARIABLES]
|
||||
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
breaks_in_ha_version="2026.6.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": INTEGRATION_TITLE,
|
||||
},
|
||||
)
|
||||
return
|
||||
charger = openevsewifi.Charger(host)
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
"deprecated_yaml",
|
||||
breaks_in_ha_version="2026.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": INTEGRATION_TITLE,
|
||||
},
|
||||
)
|
||||
entities = [
|
||||
OpenEVSESensor(charger, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in monitored_variables
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add sensors for passed config_entry in HA."""
|
||||
async_add_entities(
|
||||
(
|
||||
OpenEVSESensor(
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.runtime_data,
|
||||
description,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
),
|
||||
True,
|
||||
)
|
||||
add_entities(entities, True)
|
||||
|
||||
|
||||
class OpenEVSESensor(SensorEntity):
|
||||
"""Implementation of an OpenEVSE sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
charger: openevsewifi.Charger,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
def __init__(self, charger, description: SensorEntityDescription) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self.host = host
|
||||
self.charger = charger
|
||||
|
||||
def update(self) -> None:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This charger is already configured",
|
||||
"unavailable_host": "Unable to connect to host"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Enter the IP Address of your openevse. Should match the address you used to set it up."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"yaml_deprecated": {
|
||||
"description": "Configuring OpenEVSE using YAML is being removed. Your existing YAML configuration has been imported into the UI automatically. Remove the `openevse` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||
"title": "OpenEVSE YAML configuration is deprecated"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.16.0"]
|
||||
"requirements": ["opower==0.15.9"]
|
||||
}
|
||||
|
||||
@@ -61,10 +61,7 @@ async def async_get_device_diagnostics(
|
||||
data["execution_history"] = [
|
||||
repr(execution)
|
||||
for execution in await client.get_execution_history()
|
||||
if any(
|
||||
command.device_url.split("#", 1)[0] == device_url.split("#", 1)[0]
|
||||
for command in execution.commands
|
||||
)
|
||||
if any(command.device_url == device_url for command in execution.commands)
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
@@ -11,13 +11,5 @@
|
||||
"reload": {
|
||||
"service": "mdi:reload"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"entered_home": {
|
||||
"trigger": "mdi:account-arrow-left"
|
||||
},
|
||||
"left_home": {
|
||||
"trigger": "mdi:account-arrow-right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted persons to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::person::title%]",
|
||||
@@ -29,42 +25,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads persons from the YAML-configuration.",
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "Person",
|
||||
"triggers": {
|
||||
"entered_home": {
|
||||
"description": "Triggers when one or more persons enter home.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::person::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::person::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Entered home"
|
||||
},
|
||||
"left_home": {
|
||||
"description": "Triggers when one or more persons leave home.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::person::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::person::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Left home"
|
||||
}
|
||||
}
|
||||
"title": "Person"
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Provides triggers for persons."""
|
||||
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_origin_state_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"entered_home": make_entity_target_state_trigger(DOMAIN, STATE_HOME),
|
||||
"left_home": make_entity_origin_state_trigger(DOMAIN, from_state=STATE_HOME),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for persons."""
|
||||
return TRIGGERS
|
||||
@@ -1,18 +0,0 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: person
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
entered_home: *trigger_common
|
||||
left_home: *trigger_common
|
||||
@@ -75,15 +75,6 @@
|
||||
},
|
||||
{
|
||||
"macaddress": "84E657*"
|
||||
},
|
||||
{
|
||||
"hostname": "ps5-*"
|
||||
},
|
||||
{
|
||||
"hostname": "ps4-*"
|
||||
},
|
||||
{
|
||||
"hostname": "ps3"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/playstation_network",
|
||||
|
||||
@@ -114,72 +114,32 @@ class PooldoseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
serial_number, api_versions, errors = await self._validate_host(host)
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=SCHEMA_DEVICE,
|
||||
errors=errors,
|
||||
# Handle API version info for error display; pass version info when available
|
||||
# or None when api_versions is None to avoid displaying version details
|
||||
description_placeholders={
|
||||
"api_version_is": api_versions.get("api_version_is") or "",
|
||||
"api_version_should": api_versions.get("api_version_should")
|
||||
or "",
|
||||
}
|
||||
if api_versions
|
||||
else None,
|
||||
)
|
||||
|
||||
await self.async_set_unique_id(serial_number, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"PoolDose {serial_number}",
|
||||
data={CONF_HOST: host},
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=SCHEMA_DEVICE,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=SCHEMA_DEVICE,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfigure to change the device host/IP for an existing entry."""
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
serial_number, api_versions, errors = await self._validate_host(host)
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=SCHEMA_DEVICE,
|
||||
errors=errors,
|
||||
# Handle API version info for error display identical to other steps
|
||||
description_placeholders={
|
||||
"api_version_is": api_versions.get("api_version_is") or "",
|
||||
"api_version_should": api_versions.get("api_version_should")
|
||||
or "",
|
||||
}
|
||||
if api_versions
|
||||
else None,
|
||||
)
|
||||
|
||||
# Ensure new serial number matches the existing entry unique_id (serial number)
|
||||
if serial_number != self._get_reconfigure_entry().unique_id:
|
||||
return self.async_abort(reason="wrong_device")
|
||||
|
||||
# Update the existing config entry with the new host and schedule reload
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates={CONF_HOST: host}
|
||||
host = user_input[CONF_HOST]
|
||||
serial_number, api_versions, errors = await self._validate_host(host)
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=SCHEMA_DEVICE,
|
||||
errors=errors,
|
||||
# Handle API version info for error display; pass version info when available
|
||||
# or None when api_versions is None to avoid displaying version details
|
||||
description_placeholders={
|
||||
"api_version_is": api_versions.get("api_version_is") or "",
|
||||
"api_version_should": api_versions.get("api_version_should") or "",
|
||||
}
|
||||
if api_versions
|
||||
else None,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
# Pre-fill with current host from the entry being reconfigured
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
SCHEMA_DEVICE, self._get_reconfigure_entry().data
|
||||
),
|
||||
await self.async_set_unique_id(serial_number, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"PoolDose {serial_number}",
|
||||
data={CONF_HOST: host},
|
||||
)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
"""Diagnostics support for Pooldose."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import PooldoseConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
"IP",
|
||||
"MAC",
|
||||
"WIFI_SSID",
|
||||
"AP_SSID",
|
||||
"SERIAL_NUMBER",
|
||||
"DEVICE_ID",
|
||||
"OWNERID",
|
||||
"NAME",
|
||||
"GROUPNAME",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: PooldoseConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"device_info": async_redact_data(coordinator.device_info, TO_REDACT),
|
||||
"data": coordinator.data,
|
||||
}
|
||||
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
@@ -53,20 +53,20 @@ rules:
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: This integration does not support dynamic device discovery, as each config entry represents a single PoolDose device with all available entities.
|
||||
comment: This integration does not support dynamic devices, as it is designed for a single PoolDose device.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: This integration does not have any identified cases where repair issues would be needed.
|
||||
comment: This integration does not provide repair issues, as it is designed for a single PoolDose device with a fixed configuration.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: This integration manages a single device per config entry, so stale device removal is not applicable.
|
||||
comment: This integration does not support stale devices, as it is designed for a single PoolDose device with a fixed configuration.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_device_info": "Unable to retrieve device information",
|
||||
"no_serial_number": "No serial number found on the device",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_device": "The provided device does not match the configured device"
|
||||
"no_serial_number": "No serial number found on the device"
|
||||
},
|
||||
"error": {
|
||||
"api_not_set": "API version not found in device response. Device firmware may not be compatible with this integration.",
|
||||
@@ -22,14 +20,6 @@
|
||||
"description": "A PoolDose device was found on your network at {ip} with MAC address {mac}.\n\nDo you want to add {name} to Home Assistant?",
|
||||
"title": "Confirm DHCP discovered PoolDose device"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::pooldose::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
|
||||
@@ -164,11 +164,7 @@ class PortainerContainerSensor(PortainerContainerEntity, BinarySensorEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.endpoint_id in self.coordinator.data
|
||||
and self.device_name in self.coordinator.data[self.endpoint_id].containers
|
||||
)
|
||||
return super().available and self.endpoint_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -113,9 +113,7 @@ class PortainerButton(PortainerContainerEntity, ButtonEntity):
|
||||
"""Trigger the Portainer button press service."""
|
||||
try:
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
self.coordinator.portainer, self.endpoint_id, self.device_id
|
||||
)
|
||||
except PortainerConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -50,7 +50,7 @@ class PortainerContainerData:
|
||||
"""Container data held by the Portainer coordinator."""
|
||||
|
||||
container: DockerContainer
|
||||
stats: DockerContainerStats | None
|
||||
stats: DockerContainerStats
|
||||
stats_pre: DockerContainerStats | None
|
||||
|
||||
|
||||
@@ -147,52 +147,47 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
docker_version = await self.portainer.docker_version(endpoint.id)
|
||||
docker_info = await self.portainer.docker_info(endpoint.id)
|
||||
|
||||
prev_endpoint = self.data.get(endpoint.id) if self.data else None
|
||||
container_map: dict[str, PortainerContainerData] = {}
|
||||
|
||||
# Map containers, started and stopped
|
||||
for container in containers:
|
||||
container_name = self._get_container_name(container.names[0])
|
||||
prev_container = (
|
||||
prev_endpoint.containers[container_name]
|
||||
if prev_endpoint
|
||||
else None
|
||||
container_stats_task = [
|
||||
(
|
||||
container,
|
||||
self.portainer.container_stats(
|
||||
endpoint_id=endpoint.id,
|
||||
container_id=container.id,
|
||||
),
|
||||
)
|
||||
container_map[container_name] = PortainerContainerData(
|
||||
container=container,
|
||||
stats=None,
|
||||
stats_pre=prev_container.stats if prev_container else None,
|
||||
)
|
||||
|
||||
# Separately fetch stats for running containers
|
||||
running_containers = [
|
||||
container
|
||||
for container in containers
|
||||
if container.state == CONTAINER_STATE_RUNNING
|
||||
]
|
||||
if running_containers:
|
||||
container_stats = dict(
|
||||
zip(
|
||||
(
|
||||
self._get_container_name(container.names[0])
|
||||
for container in running_containers
|
||||
),
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.portainer.container_stats(
|
||||
endpoint_id=endpoint.id,
|
||||
container_id=container.id,
|
||||
)
|
||||
for container in running_containers
|
||||
)
|
||||
),
|
||||
strict=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Now assign stats to the containers
|
||||
for container_name, stats in container_stats.items():
|
||||
container_map[container_name].stats = stats
|
||||
container_stats_gather = await asyncio.gather(
|
||||
*[task for _, task in container_stats_task]
|
||||
)
|
||||
for (container, _), container_stats in zip(
|
||||
container_stats_task, container_stats_gather, strict=False
|
||||
):
|
||||
container_name = container.names[0].replace("/", " ").strip()
|
||||
|
||||
# Store previous stats if available. This is used to calculate deltas for CPU and network usage
|
||||
# In the first call it will be None, since it has nothing to compare with
|
||||
# Added a walrus pattern to check if not None on prev_container, to keep mypy happy. :)
|
||||
container_map[container_name] = PortainerContainerData(
|
||||
container=container,
|
||||
stats=container_stats,
|
||||
stats_pre=(
|
||||
prev_container.stats
|
||||
if self.data
|
||||
and (prev_data := self.data.get(endpoint.id)) is not None
|
||||
and (
|
||||
prev_container := prev_data.containers.get(
|
||||
container_name
|
||||
)
|
||||
)
|
||||
is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
except PortainerConnectionError as err:
|
||||
_LOGGER.exception("Connection error")
|
||||
raise UpdateFailed(
|
||||
@@ -233,15 +228,11 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
|
||||
# Surprise, we also handle containers here :)
|
||||
current_containers = {
|
||||
(endpoint.id, container_name)
|
||||
(endpoint.id, container.container.id)
|
||||
for endpoint in mapped_endpoints.values()
|
||||
for container_name in endpoint.containers
|
||||
for container in endpoint.containers.values()
|
||||
}
|
||||
new_containers = current_containers - self.known_containers
|
||||
if new_containers:
|
||||
_LOGGER.debug("New containers found: %s", new_containers)
|
||||
self.known_containers.update(new_containers)
|
||||
|
||||
def _get_container_name(self, container_name: str) -> str:
|
||||
"""Sanitize to get a proper container name."""
|
||||
return container_name.replace("/", " ").strip()
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"architecture": {
|
||||
"default": "mdi:cpu-64-bit"
|
||||
},
|
||||
"container_state": {
|
||||
"default": "mdi:state-machine"
|
||||
},
|
||||
"containers_count": {
|
||||
"default": "mdi:database"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.22"]
|
||||
"requirements": ["pyportainer==1.0.19"]
|
||||
}
|
||||
|
||||
@@ -49,19 +49,10 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
translation_key="image",
|
||||
value_fn=lambda data: data.container.image,
|
||||
),
|
||||
PortainerContainerSensorEntityDescription(
|
||||
key="container_state",
|
||||
translation_key="container_state",
|
||||
value_fn=lambda data: data.container.state,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["running", "exited", "paused", "restarting", "created", "dead"],
|
||||
),
|
||||
PortainerContainerSensorEntityDescription(
|
||||
key="memory_limit",
|
||||
translation_key="memory_limit",
|
||||
value_fn=lambda data: (
|
||||
data.stats.memory_stats.limit if data.stats is not None else 0
|
||||
),
|
||||
value_fn=lambda data: data.stats.memory_stats.limit,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
|
||||
@@ -72,9 +63,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
PortainerContainerSensorEntityDescription(
|
||||
key="memory_usage",
|
||||
translation_key="memory_usage",
|
||||
value_fn=lambda data: (
|
||||
data.stats.memory_stats.usage if data.stats is not None else 0
|
||||
),
|
||||
value_fn=lambda data: data.stats.memory_stats.usage,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
|
||||
@@ -87,9 +76,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
translation_key="memory_usage_percentage",
|
||||
value_fn=lambda data: (
|
||||
(data.stats.memory_stats.usage / data.stats.memory_stats.limit) * 100.0
|
||||
if data.stats is not None
|
||||
and data.stats.memory_stats.limit > 0
|
||||
and data.stats.memory_stats.usage > 0
|
||||
if data.stats.memory_stats.limit > 0 and data.stats.memory_stats.usage > 0
|
||||
else 0.0
|
||||
),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
@@ -102,8 +89,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
translation_key="cpu_usage_total",
|
||||
value_fn=lambda data: (
|
||||
(total_delta / system_delta) * data.stats.cpu_stats.online_cpus * 100.0
|
||||
if data.stats is not None
|
||||
and (prev := data.stats_pre) is not None
|
||||
if (prev := data.stats_pre) is not None
|
||||
and (
|
||||
system_delta := (
|
||||
data.stats.cpu_stats.system_cpu_usage
|
||||
@@ -261,6 +247,7 @@ async def async_setup_entry(
|
||||
)
|
||||
for (endpoint, container) in containers
|
||||
for entity_description in CONTAINER_SENSORS
|
||||
if entity_description.value_fn(container) is not None
|
||||
)
|
||||
|
||||
coordinator.new_endpoints_callbacks.append(_async_add_new_endpoints)
|
||||
@@ -303,11 +290,7 @@ class PortainerContainerSensor(PortainerContainerEntity, SensorEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.endpoint_id in self.coordinator.data
|
||||
and self.device_name in self.coordinator.data[self.endpoint_id].containers
|
||||
)
|
||||
return super().available and self.endpoint_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
|
||||
@@ -68,17 +68,6 @@
|
||||
"architecture": {
|
||||
"name": "Architecture"
|
||||
},
|
||||
"container_state": {
|
||||
"name": "State",
|
||||
"state": {
|
||||
"created": "Created",
|
||||
"dead": "Dead",
|
||||
"exited": "Exited",
|
||||
"paused": "Paused",
|
||||
"restarting": "Restarting",
|
||||
"running": "Running"
|
||||
}
|
||||
},
|
||||
"containers_count": {
|
||||
"name": "Container count"
|
||||
},
|
||||
|
||||
@@ -137,19 +137,13 @@ class PortainerContainerSwitch(PortainerContainerEntity, SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start (turn on) the container."""
|
||||
await self.entity_description.turn_on_fn(
|
||||
"start",
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
"start", self.coordinator.portainer, self.endpoint_id, self.device_id
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop (turn off) the container."""
|
||||
await self.entity_description.turn_off_fn(
|
||||
"stop",
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
"stop", self.coordinator.portainer, self.endpoint_id, self.device_id
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -8,7 +8,7 @@ from httpx import HTTPError, InvalidURL, TimeoutException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .client import get_calendar
|
||||
@@ -21,7 +21,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CALENDAR_NAME): str,
|
||||
vol.Required(CONF_URL): str,
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -49,7 +48,7 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"webcal://", "https://", 1
|
||||
)
|
||||
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
|
||||
client = get_async_client(self.hass, verify_ssl=user_input[CONF_VERIFY_SSL])
|
||||
client = get_async_client(self.hass)
|
||||
try:
|
||||
res = await get_calendar(client, user_input[CONF_URL])
|
||||
if res.status_code == HTTPStatus.FORBIDDEN:
|
||||
|
||||
@@ -7,7 +7,7 @@ from httpx import HTTPError, InvalidURL, TimeoutException
|
||||
from ical.calendar import Calendar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -42,9 +42,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
||||
config_entry=config_entry,
|
||||
always_update=True,
|
||||
)
|
||||
self._client = get_async_client(
|
||||
hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True)
|
||||
)
|
||||
self._client = get_async_client(hass)
|
||||
self._url = config_entry.data[CONF_URL]
|
||||
|
||||
async def _async_update_data(self) -> Calendar:
|
||||
|
||||
@@ -13,13 +13,11 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"calendar_name": "Calendar name",
|
||||
"url": "Calendar URL",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
"url": "Calendar URL"
|
||||
},
|
||||
"data_description": {
|
||||
"calendar_name": "The name of the calendar shown in the UI.",
|
||||
"url": "The URL of the remote calendar.",
|
||||
"verify_ssl": "Enable SSL certificate verification for secure connections."
|
||||
"url": "The URL of the remote calendar."
|
||||
},
|
||||
"description": "Please choose a name for the calendar to be imported"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components.light import (
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .entity import (
|
||||
ReolinkChannelCoordinatorEntity,
|
||||
@@ -158,16 +157,16 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this light between 1.255."""
|
||||
"""Return the brightness of this light between 0.255."""
|
||||
assert self.entity_description.get_brightness_fn is not None
|
||||
|
||||
bright_pct = self.entity_description.get_brightness_fn(
|
||||
self._host.api, self._channel
|
||||
)
|
||||
if not bright_pct:
|
||||
if bright_pct is None:
|
||||
return None
|
||||
|
||||
return color_util.value_to_brightness((1, 100), bright_pct)
|
||||
return round(255 * bright_pct / 100.0)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
@@ -190,7 +189,7 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
if (
|
||||
brightness := kwargs.get(ATTR_BRIGHTNESS)
|
||||
) is not None and self.entity_description.set_brightness_fn is not None:
|
||||
brightness_pct = round(color_util.brightness_to_value((1, 100), brightness))
|
||||
brightness_pct = int(brightness / 255.0 * 100)
|
||||
await self.entity_description.set_brightness_fn(
|
||||
self._host.api, self._channel, brightness_pct
|
||||
)
|
||||
|
||||
@@ -552,7 +552,6 @@ class RoborockB01Q7UpdateCoordinator(RoborockDataUpdateCoordinatorB01):
|
||||
RoborockB01Props.CLEANING_TIME,
|
||||
RoborockB01Props.REAL_CLEAN_TIME,
|
||||
RoborockB01Props.HYPA,
|
||||
RoborockB01Props.WIND,
|
||||
]
|
||||
|
||||
async def _async_update_data(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==4.2.0",
|
||||
"python-roborock==4.1.0",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from roborock.data import RoborockStateCode, SCWindMapping, WorkStatusMapping
|
||||
from roborock.data import RoborockStateCode
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
import voluptuous as vol
|
||||
@@ -24,12 +24,8 @@ from .const import (
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
)
|
||||
from .coordinator import (
|
||||
RoborockB01Q7UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockDataUpdateCoordinator,
|
||||
)
|
||||
from .entity import RoborockCoordinatedEntityB01, RoborockCoordinatedEntityV1
|
||||
from .coordinator import RoborockConfigEntry, RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockCoordinatedEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,20 +57,6 @@ STATE_CODE_TO_STATE = {
|
||||
RoborockStateCode.device_offline: VacuumActivity.ERROR, # "Device offline"
|
||||
}
|
||||
|
||||
Q7_STATE_CODE_TO_STATE = {
|
||||
WorkStatusMapping.SLEEPING: VacuumActivity.IDLE,
|
||||
WorkStatusMapping.WAITING_FOR_ORDERS: VacuumActivity.IDLE,
|
||||
WorkStatusMapping.PAUSED: VacuumActivity.PAUSED,
|
||||
WorkStatusMapping.DOCKING: VacuumActivity.RETURNING,
|
||||
WorkStatusMapping.CHARGING: VacuumActivity.DOCKED,
|
||||
WorkStatusMapping.SWEEP_MOPING: VacuumActivity.CLEANING,
|
||||
WorkStatusMapping.SWEEP_MOPING_2: VacuumActivity.CLEANING,
|
||||
WorkStatusMapping.MOPING: VacuumActivity.CLEANING,
|
||||
WorkStatusMapping.UPDATING: VacuumActivity.DOCKED,
|
||||
WorkStatusMapping.MOP_CLEANING: VacuumActivity.DOCKED,
|
||||
WorkStatusMapping.MOP_AIRDRYING: VacuumActivity.DOCKED,
|
||||
}
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@@ -87,11 +69,6 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
RoborockVacuum(coordinator) for coordinator in config_entry.runtime_data.v1
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockQ7Vacuum(coordinator)
|
||||
for coordinator in config_entry.runtime_data.b01
|
||||
if isinstance(coordinator, RoborockB01Q7UpdateCoordinator)
|
||||
)
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
platform.async_register_entity_service(
|
||||
@@ -264,149 +241,3 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
"x": robot_position.x,
|
||||
"y": robot_position.y,
|
||||
}
|
||||
|
||||
|
||||
class RoborockQ7Vacuum(RoborockCoordinatedEntityB01, StateVacuumEntity):
|
||||
"""General Representation of a Roborock vacuum."""
|
||||
|
||||
_attr_icon = "mdi:robot-vacuum"
|
||||
_attr_supported_features = (
|
||||
VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.FAN_SPEED
|
||||
| VacuumEntityFeature.SEND_COMMAND
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.STATE
|
||||
| VacuumEntityFeature.START
|
||||
)
|
||||
_attr_translation_key = DOMAIN
|
||||
_attr_name = None
|
||||
coordinator: RoborockB01Q7UpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RoborockB01Q7UpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize a vacuum."""
|
||||
StateVacuumEntity.__init__(self)
|
||||
RoborockCoordinatedEntityB01.__init__(
|
||||
self,
|
||||
coordinator.duid_slug,
|
||||
coordinator,
|
||||
)
|
||||
|
||||
@property
|
||||
def fan_speed_list(self) -> list[str]:
|
||||
"""Get the list of available fan speeds."""
|
||||
return SCWindMapping.keys()
|
||||
|
||||
@property
|
||||
def activity(self) -> VacuumActivity | None:
|
||||
"""Return the status of the vacuum cleaner."""
|
||||
if self.coordinator.data.status is not None:
|
||||
return Q7_STATE_CODE_TO_STATE.get(self.coordinator.data.status)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str | None:
|
||||
"""Return the fan speed of the vacuum cleaner."""
|
||||
return self.coordinator.data.wind_name
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start the vacuum."""
|
||||
try:
|
||||
await self.coordinator.api.start_clean()
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "start_clean",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_pause(self) -> None:
|
||||
"""Pause the vacuum."""
|
||||
try:
|
||||
await self.coordinator.api.pause_clean()
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "pause_clean",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_stop(self, **kwargs: Any) -> None:
|
||||
"""Stop the vacuum."""
|
||||
try:
|
||||
await self.coordinator.api.stop_clean()
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "stop_clean",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_return_to_base(self, **kwargs: Any) -> None:
|
||||
"""Send vacuum back to base."""
|
||||
try:
|
||||
await self.coordinator.api.return_to_dock()
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "return_to_dock",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_locate(self, **kwargs: Any) -> None:
|
||||
"""Locate vacuum."""
|
||||
try:
|
||||
await self.coordinator.api.find_me()
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "find_me",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
"""Set vacuum fan speed."""
|
||||
try:
|
||||
await self.coordinator.api.set_fan_speed(
|
||||
SCWindMapping.from_value(fan_speed)
|
||||
)
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "set_fan_speed",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_send_command(
|
||||
self,
|
||||
command: str,
|
||||
params: dict[str, Any] | list[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Send a command to a vacuum cleaner."""
|
||||
try:
|
||||
await self.coordinator.api.send(command, params)
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": command,
|
||||
},
|
||||
) from err
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.7.2",
|
||||
"wakeonlan==3.1.0",
|
||||
"async-upnp-client==0.46.2"
|
||||
"async-upnp-client==0.46.1"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -442,7 +442,7 @@ class BlockSleepingClimate(
|
||||
self._attr_device_info = get_entity_block_device_info(coordinator, sensor_block)
|
||||
self._attr_name = None # Main device entity
|
||||
|
||||
self._channel = int(self._unique_id.split("_")[1])
|
||||
self._channel = cast(int, self._unique_id.split("_")[1])
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self) -> ShellyClimateExtraStoredData:
|
||||
@@ -543,9 +543,8 @@ class BlockSleepingClimate(
|
||||
"""Set block state (HTTP request)."""
|
||||
LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs)
|
||||
try:
|
||||
return await self.coordinator.device.set_thermostat_state(
|
||||
self._channel,
|
||||
**kwargs,
|
||||
return await self.coordinator.device.http_request(
|
||||
"get", f"thermostat/{self._channel}", kwargs
|
||||
)
|
||||
except DeviceConnectionError as err:
|
||||
self.coordinator.last_update_success = False
|
||||
@@ -578,7 +577,7 @@ class BlockSleepingClimate(
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
)
|
||||
|
||||
await self.set_state_full_path(target_t_enabled=1, target_t=target_temp)
|
||||
await self.set_state_full_path(target_t_enabled=1, target_t=f"{target_temp}")
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
@@ -586,7 +585,7 @@ class BlockSleepingClimate(
|
||||
if isinstance(self.target_temperature, float):
|
||||
self._last_target_temp = self.target_temperature
|
||||
await self.set_state_full_path(
|
||||
target_t_enabled=1, target_t=self._attr_min_temp
|
||||
target_t_enabled=1, target_t=f"{self._attr_min_temp}"
|
||||
)
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
await self.set_state_full_path(
|
||||
@@ -600,7 +599,9 @@ class BlockSleepingClimate(
|
||||
if preset_index == 0:
|
||||
await self.set_state_full_path(schedule=0)
|
||||
else:
|
||||
await self.set_state_full_path(schedule=1, schedule_profile=preset_index)
|
||||
await self.set_state_full_path(
|
||||
schedule=1, schedule_profile=f"{preset_index}"
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
||||
@@ -325,7 +325,8 @@ class ShoppingData:
|
||||
)
|
||||
return self.items
|
||||
|
||||
async def async_reorder(
|
||||
@callback
|
||||
def async_reorder(
|
||||
self, item_ids: list[str], context: Context | None = None
|
||||
) -> None:
|
||||
"""Reorder items."""
|
||||
@@ -350,7 +351,7 @@ class ShoppingData:
|
||||
)
|
||||
new_items.append(value)
|
||||
self.items = new_items
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
@@ -387,7 +388,7 @@ class ShoppingData:
|
||||
) -> None:
|
||||
"""Sort items by name."""
|
||||
self.items = sorted(self.items, key=lambda item: item["name"], reverse=reverse) # type: ignore[arg-type,return-value]
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
@@ -590,8 +591,7 @@ async def websocket_handle_clear(
|
||||
vol.Required("item_ids"): [str],
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_handle_reorder(
|
||||
def websocket_handle_reorder(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -599,9 +599,7 @@ async def websocket_handle_reorder(
|
||||
"""Handle reordering shopping_list items."""
|
||||
msg_id = msg.pop("id")
|
||||
try:
|
||||
await hass.data[DOMAIN].async_reorder(
|
||||
msg.pop("item_ids"), connection.context(msg)
|
||||
)
|
||||
hass.data[DOMAIN].async_reorder(msg.pop("item_ids"), connection.context(msg))
|
||||
except NoMatchingShoppingListItem:
|
||||
connection.send_error(
|
||||
msg_id,
|
||||
|
||||
@@ -37,7 +37,6 @@ SONOS_RADIO = "radio"
|
||||
SONOS_SHARE = "share"
|
||||
SONOS_OTHER_ITEM = "other items"
|
||||
SONOS_AUDIO_BOOK = "audio book"
|
||||
SONOS_PODCAST = "podcast"
|
||||
|
||||
MEDIA_TYPE_DIRECTORY = MediaClass.DIRECTORY
|
||||
|
||||
@@ -67,7 +66,6 @@ SONOS_TO_MEDIA_CLASSES = {
|
||||
SONOS_COMPOSER: MediaClass.COMPOSER,
|
||||
SONOS_GENRE: MediaClass.GENRE,
|
||||
SONOS_PLAYLISTS: MediaClass.PLAYLIST,
|
||||
SONOS_PODCAST: MediaClass.PODCAST,
|
||||
SONOS_TRACKS: MediaClass.TRACK,
|
||||
SONOS_SHARE: MediaClass.DIRECTORY,
|
||||
"object.container": MediaClass.DIRECTORY,
|
||||
@@ -77,7 +75,6 @@ SONOS_TO_MEDIA_CLASSES = {
|
||||
"object.container.person.musicArtist": MediaClass.ARTIST,
|
||||
"object.container.playlistContainer.sameArtist": MediaClass.ARTIST,
|
||||
"object.container.playlistContainer": MediaClass.PLAYLIST,
|
||||
"object.container.podcast": MediaClass.PODCAST,
|
||||
"object.item": MediaClass.TRACK,
|
||||
"object.item.audioItem.musicTrack": MediaClass.TRACK,
|
||||
"object.item.audioItem.audioBroadcast": MediaClass.GENRE,
|
||||
@@ -91,7 +88,6 @@ SONOS_TO_MEDIA_TYPES = {
|
||||
SONOS_COMPOSER: MediaType.COMPOSER,
|
||||
SONOS_GENRE: MediaType.GENRE,
|
||||
SONOS_PLAYLISTS: MediaType.PLAYLIST,
|
||||
SONOS_PODCAST: MediaType.PODCAST,
|
||||
SONOS_TRACKS: MediaType.TRACK,
|
||||
"object.container": MEDIA_TYPE_DIRECTORY,
|
||||
"object.container.album.musicAlbum": MediaType.ALBUM,
|
||||
@@ -100,7 +96,6 @@ SONOS_TO_MEDIA_TYPES = {
|
||||
"object.container.person.musicArtist": MediaType.ARTIST,
|
||||
"object.container.playlistContainer.sameArtist": MediaType.ARTIST,
|
||||
"object.container.playlistContainer": MediaType.PLAYLIST,
|
||||
"object.container.podcast": MediaType.PODCAST,
|
||||
"object.item.audioItem.musicTrack": MediaType.TRACK,
|
||||
"object.item.audioItem.audioBook": MediaType.TRACK,
|
||||
}
|
||||
@@ -130,7 +125,6 @@ SONOS_TYPES_MAPPING = {
|
||||
"object.container.person.musicArtist": SONOS_ALBUM_ARTIST,
|
||||
"object.container.playlistContainer.sameArtist": SONOS_ARTIST,
|
||||
"object.container.playlistContainer": SONOS_PLAYLISTS,
|
||||
"object.container.podcast": SONOS_PODCAST,
|
||||
"object.item": SONOS_OTHER_ITEM,
|
||||
"object.item.audioItem.musicTrack": SONOS_TRACKS,
|
||||
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
|
||||
@@ -155,7 +149,6 @@ PLAYABLE_MEDIA_TYPES = [
|
||||
MediaType.CONTRIBUTING_ARTIST,
|
||||
MediaType.GENRE,
|
||||
MediaType.PLAYLIST,
|
||||
MediaType.PODCAST,
|
||||
MediaType.TRACK,
|
||||
]
|
||||
|
||||
@@ -212,4 +205,3 @@ BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15)
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
DISCOVERY_INTERVAL = datetime.timedelta(seconds=60)
|
||||
SUBSCRIPTION_TIMEOUT = 1200
|
||||
LONG_SERVICE_TIMEOUT = 30.0
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "bronze",
|
||||
"requirements": [
|
||||
"defusedxml==0.7.1",
|
||||
"soco==0.30.14",
|
||||
"soco==0.30.13",
|
||||
"sonos-websocket==0.1.3"
|
||||
],
|
||||
"ssdp": [
|
||||
|
||||
@@ -54,7 +54,6 @@ from . import media_browser
|
||||
from .const import (
|
||||
ATTR_QUEUE_POSITION,
|
||||
DOMAIN,
|
||||
LONG_SERVICE_TIMEOUT,
|
||||
MEDIA_TYPE_DIRECTORY,
|
||||
MEDIA_TYPES_TO_SONOS,
|
||||
MODELS_LINEIN_AND_TV,
|
||||
@@ -77,6 +76,7 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LONG_SERVICE_TIMEOUT = 30.0
|
||||
UNJOIN_SERVICE_TIMEOUT = 0.1
|
||||
VOLUME_INCREMENT = 2
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ from .const import (
|
||||
AVAILABILITY_TIMEOUT,
|
||||
BATTERY_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
LONG_SERVICE_TIMEOUT,
|
||||
SCAN_INTERVAL,
|
||||
SONOS_CHECK_ACTIVITY,
|
||||
SONOS_CREATE_ALARM,
|
||||
@@ -959,23 +958,6 @@ class SonosSpeaker:
|
||||
# as those "invisible" speakers will bypass the single speaker check
|
||||
return
|
||||
|
||||
# Clear coordinator on speakers that are no longer in this group
|
||||
old_members = set(self.sonos_group[1:])
|
||||
new_members = set(sonos_group[1:])
|
||||
removed_members = old_members - new_members
|
||||
for removed_speaker in removed_members:
|
||||
# Only clear if this speaker was coordinated by self and in the same group
|
||||
if (
|
||||
removed_speaker.coordinator == self
|
||||
and removed_speaker.sonos_group is self.sonos_group
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Zone %s Cleared coordinator [%s] (removed from group)",
|
||||
removed_speaker.zone_name,
|
||||
self.zone_name,
|
||||
)
|
||||
removed_speaker.clear_coordinator()
|
||||
|
||||
self.coordinator = None
|
||||
self.sonos_group = sonos_group
|
||||
self.sonos_group_entities = sonos_group_entities
|
||||
@@ -1008,19 +990,6 @@ class SonosSpeaker:
|
||||
|
||||
return _async_handle_group_event(event)
|
||||
|
||||
@callback
|
||||
def clear_coordinator(self) -> None:
|
||||
"""Clear coordinator from speaker."""
|
||||
self.coordinator = None
|
||||
self.sonos_group = [self]
|
||||
entity_registry = er.async_get(self.hass)
|
||||
speaker_entity_id = cast(
|
||||
str,
|
||||
entity_registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.uid),
|
||||
)
|
||||
self.sonos_group_entities = [speaker_entity_id]
|
||||
self.async_write_entity_states()
|
||||
|
||||
@soco_error()
|
||||
def join(self, speakers: list[SonosSpeaker]) -> list[SonosSpeaker]:
|
||||
"""Form a group with other players."""
|
||||
@@ -1068,7 +1037,8 @@ class SonosSpeaker:
|
||||
"""Unjoin the player from a group."""
|
||||
if self.sonos_group == [self]:
|
||||
return
|
||||
self.soco.unjoin(timeout=LONG_SERVICE_TIMEOUT)
|
||||
self.soco.unjoin()
|
||||
self.coordinator = None
|
||||
|
||||
@staticmethod
|
||||
async def unjoin_multi(
|
||||
|
||||
@@ -7,14 +7,9 @@ from typing import Any
|
||||
from srpenergy.client import SrpEnergyClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import CONF_IS_TOU, DOMAIN, LOGGER
|
||||
@@ -45,71 +40,52 @@ class SRPEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@callback
|
||||
def _show_form(self, errors: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Show the form to the user."""
|
||||
LOGGER.debug("Show Form")
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_NAME, default=self.hass.config.location_name
|
||||
): str,
|
||||
vol.Required(CONF_ID): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_IS_TOU, default=False): bool,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
LOGGER.debug("Config entry")
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except ValueError:
|
||||
# Thrown when the account id is malformed
|
||||
errors["base"] = "invalid_account"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_ID])
|
||||
if self.source == SOURCE_USER:
|
||||
self._abort_if_unique_id_configured()
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
if not user_input:
|
||||
return self._show_form(errors)
|
||||
|
||||
if self.source == SOURCE_USER:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME],
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ID): (
|
||||
str
|
||||
if self.source == SOURCE_USER
|
||||
else self._get_reconfigure_entry().data[CONF_ID]
|
||||
),
|
||||
vol.Required(
|
||||
CONF_NAME, default=self.hass.config.location_name
|
||||
): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_IS_TOU, default=False): bool,
|
||||
}
|
||||
),
|
||||
suggested_values=(
|
||||
user_input or self._get_reconfigure_entry().data
|
||||
if self.source == SOURCE_RECONFIGURE
|
||||
else None
|
||||
),
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except ValueError:
|
||||
# Thrown when the account id is malformed
|
||||
errors["base"] = "invalid_account"
|
||||
return self._show_form(errors)
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
return self._show_form(errors)
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration."""
|
||||
return await self.async_step_user()
|
||||
await self.async_set_unique_id(user_input[CONF_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["async-upnp-client==0.46.2"]
|
||||
"requirements": ["async-upnp-client==0.46.1"]
|
||||
}
|
||||
|
||||
@@ -171,7 +171,6 @@ async def make_device_data(
|
||||
"K20+ Pro",
|
||||
"Robot Vacuum Cleaner K10+ Pro Combo",
|
||||
"Robot Vacuum Cleaner S10",
|
||||
"Robot Vacuum Cleaner S20",
|
||||
"S20",
|
||||
"Robot Vacuum Cleaner K11 Plus",
|
||||
]:
|
||||
@@ -280,9 +279,6 @@ async def make_device_data(
|
||||
"RGBICWW Strip Light",
|
||||
"Ceiling Light",
|
||||
"Ceiling Light Pro",
|
||||
"RGBIC Neon Rope Light",
|
||||
"RGBIC Neon Wire Rope Light",
|
||||
"Candle Warmer Lamp",
|
||||
]:
|
||||
coordinator = await coordinator_for_device(
|
||||
hass, entry, api, device, coordinators_by_id
|
||||
|
||||
@@ -132,46 +132,15 @@ class SwitchBotCloudLight(SwitchBotCloudEntity, LightEntity):
|
||||
)
|
||||
|
||||
|
||||
class SwitchBotCloudCandleWarmerLamp(SwitchBotCloudLight):
|
||||
"""Representation of a SwitchBotCloud CandleWarmerLamp."""
|
||||
|
||||
# Brightness adjustment
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
|
||||
class SwitchBotCloudStripLight(SwitchBotCloudLight):
|
||||
"""Representation of a SwitchBot Strip Light."""
|
||||
|
||||
# Brightness adjustment
|
||||
# RGB color control
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.RGB}
|
||||
|
||||
|
||||
class SwitchBotCloudRGBICLight(SwitchBotCloudLight):
|
||||
"""Representation of a SwitchBotCloudRGBICLight."""
|
||||
|
||||
# Brightness adjustment
|
||||
# RGB color control
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.RGB}
|
||||
|
||||
async def _send_rgb_color_command(self, rgb_color: tuple) -> None:
|
||||
"""Send an RGB command."""
|
||||
await self.send_api_command(
|
||||
RGBWLightCommands.SET_COLOR,
|
||||
parameters=f"{rgb_color[0]}:{rgb_color[1]}:{rgb_color[2]}",
|
||||
)
|
||||
|
||||
|
||||
class SwitchBotCloudRGBWWLight(SwitchBotCloudLight):
|
||||
"""Representation of SwitchBot |Strip Light|Floor Lamp|Color Bulb."""
|
||||
|
||||
# Brightness adjustment
|
||||
# RGB color control
|
||||
# Color temperature control
|
||||
|
||||
_attr_max_color_temp_kelvin = 6500
|
||||
_attr_min_color_temp_kelvin = 2700
|
||||
|
||||
@@ -195,9 +164,6 @@ class SwitchBotCloudRGBWWLight(SwitchBotCloudLight):
|
||||
class SwitchBotCloudCeilingLight(SwitchBotCloudLight):
|
||||
"""Representation of SwitchBot Ceiling Light."""
|
||||
|
||||
# Brightness adjustment
|
||||
# Color temperature control
|
||||
|
||||
_attr_max_color_temp_kelvin = 6500
|
||||
_attr_min_color_temp_kelvin = 2700
|
||||
|
||||
@@ -221,14 +187,10 @@ class SwitchBotCloudCeilingLight(SwitchBotCloudLight):
|
||||
@callback
|
||||
def _async_make_entity(
|
||||
api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator
|
||||
) -> SwitchBotCloudLight:
|
||||
) -> SwitchBotCloudStripLight | SwitchBotCloudRGBWWLight | SwitchBotCloudCeilingLight:
|
||||
"""Make a SwitchBotCloudLight."""
|
||||
if device.device_type == "Strip Light":
|
||||
return SwitchBotCloudStripLight(api, device, coordinator)
|
||||
if device.device_type in ["Ceiling Light", "Ceiling Light Pro"]:
|
||||
return SwitchBotCloudCeilingLight(api, device, coordinator)
|
||||
if device.device_type == "Candle Warmer Lamp":
|
||||
return SwitchBotCloudCandleWarmerLamp(api, device, coordinator)
|
||||
if device.device_type in ["RGBIC Neon Rope Light", "RGBIC Neon Wire Rope Light"]:
|
||||
return SwitchBotCloudRGBICLight(api, device, coordinator)
|
||||
return SwitchBotCloudRGBWWLight(api, device, coordinator)
|
||||
|
||||
@@ -245,9 +245,6 @@ def _async_make_entity(
|
||||
return SwitchBotCloudVacuumV2(api, device, coordinator)
|
||||
if device.device_type == "Robot Vacuum Cleaner K10+ Pro Combo":
|
||||
return SwitchBotCloudVacuumK10PlusProCombo(api, device, coordinator)
|
||||
if (
|
||||
device.device_type in VacuumCleanerV3Commands.get_supported_devices()
|
||||
or device.device_type == "Robot Vacuum Cleaner S20"
|
||||
):
|
||||
if device.device_type in VacuumCleanerV3Commands.get_supported_devices():
|
||||
return SwitchBotCloudVacuumV3(api, device, coordinator)
|
||||
return SwitchBotCloudVacuum(api, device, coordinator)
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: todo
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
`test_user` initializes flow with `None` data
|
||||
`test_user` imports a fixture that already patches, but then patches again
|
||||
`test_user` doesn't continue the old flow but creates a second one
|
||||
`test_user` can be parametrized to test the false SSL part
|
||||
`test_user_2sa` directly initialized the flow with form data
|
||||
Flows should end in CREATE_ENTRY or ABORT
|
||||
dependency-transparency: done
|
||||
docs-actions: todo
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name:
|
||||
status: todo
|
||||
comment: button entities missing
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
Handled by coordinator.
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
consts.py -> const.py
|
||||
fixture could be autospecced and also be combined with the config flow one
|
||||
Consider creating a fixture of the mock config entry
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: done
|
||||
comment: Could add serial number to camera device
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: todo
|
||||
comment: cameras and disks can be replaced and removed
|
||||
entity-category:
|
||||
status: todo
|
||||
comment: CPU load sounds like diagnostic data
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations:
|
||||
status: todo
|
||||
comment: button still has names, can use placeholders
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: todo
|
||||
comment: button still has icons
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: see dynamic-devices
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing:
|
||||
status: done
|
||||
comment: Would be nice if we can get rid of getattr
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user