Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ on:
type: boolean

env:
CACHE_VERSION: 3
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.1"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/airobot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/airobot/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entity": {
"number": {
"hysteresis_band": {
"default": "mdi:delta"
}
}
}
}
99 changes: 99 additions & 0 deletions homeassistant/components/airobot/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Number platform for Airobot thermostat."""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from dataclasses import dataclass

from pyairobotrest.const import HYSTERESIS_BAND_MAX, HYSTERESIS_BAND_MIN
from pyairobotrest.exceptions import AirobotError

from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import AirobotConfigEntry
from .const import DOMAIN
from .coordinator import AirobotDataUpdateCoordinator
from .entity import AirobotEntity

PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class AirobotNumberEntityDescription(NumberEntityDescription):
"""Describes Airobot number entity."""

value_fn: Callable[[AirobotDataUpdateCoordinator], float]
set_value_fn: Callable[[AirobotDataUpdateCoordinator, float], Awaitable[None]]


NUMBERS: tuple[AirobotNumberEntityDescription, ...] = (
AirobotNumberEntityDescription(
key="hysteresis_band",
translation_key="hysteresis_band",
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
native_min_value=HYSTERESIS_BAND_MIN / 10.0,
native_max_value=HYSTERESIS_BAND_MAX / 10.0,
native_step=0.1,
value_fn=lambda coordinator: coordinator.data.settings.hysteresis_band,
set_value_fn=lambda coordinator, value: coordinator.client.set_hysteresis_band(
value
),
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot number platform."""
coordinator = entry.runtime_data
async_add_entities(
AirobotNumber(coordinator, description) for description in NUMBERS
)


class AirobotNumber(AirobotEntity, NumberEntity):
"""Representation of an Airobot number entity."""

entity_description: AirobotNumberEntityDescription

def __init__(
self,
coordinator: AirobotDataUpdateCoordinator,
description: AirobotNumberEntityDescription,
) -> None:
"""Initialize the number entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"

@property
def native_value(self) -> float:
"""Return the current value."""
return self.entity_description.value_fn(self.coordinator)

async def async_set_native_value(self, value: float) -> None:
"""Set the value."""
try:
await self.entity_description.set_value_fn(self.coordinator, value)
except AirobotError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="set_value_failed",
translation_placeholders={"error": str(err)},
) from err
else:
await self.coordinator.async_request_refresh()
4 changes: 2 additions & 2 deletions homeassistant/components/airobot/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ rules:
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: Single device integration, no dynamic device discovery needed.
Expand All @@ -57,7 +57,7 @@ rules:
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/airobot/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
}
},
"entity": {
"number": {
"hysteresis_band": {
"name": "Hysteresis band"
}
},
"sensor": {
"air_temperature": {
"name": "Air temperature"
Expand Down Expand Up @@ -74,6 +79,9 @@
},
"set_temperature_failed": {
"message": "Failed to set temperature to {temperature}."
},
"set_value_failed": {
"message": "Failed to set value: {error}"
}
}
}
11 changes: 8 additions & 3 deletions homeassistant/components/huawei_lte/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,10 +773,15 @@ async def async_setup_entry(
continue
if key_meta := SENSOR_META.get(key):
if key_meta.include:
items = filter(key_meta.include.search, items)
items = {k: v for k, v in items.items() if key_meta.include.search(k)}
if key_meta.exclude:
items = [x for x in items if not key_meta.exclude.search(x)]
for item in items:
items = {
k: v for k, v in items.items() if not key_meta.exclude.search(k)
}
for item, value in items.items():
if value is None:
_LOGGER.debug("Ignoring sensor %s.%s due to None value", key, item)
continue
if not (desc := SENSOR_META[key].descriptions.get(item)):
_LOGGER.debug( # pylint: disable=hass-logger-period # false positive
(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/roborock/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==3.19.0",
"python-roborock==3.20.1",
"vacuum-map-parser-roborock==0.1.4"
]
}
1 change: 1 addition & 0 deletions homeassistant/components/saunum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .coordinator import LeilSaunaCoordinator

PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.LIGHT,
Platform.SENSOR,
Expand Down
120 changes: 120 additions & 0 deletions homeassistant/components/saunum/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Binary sensor platform for Saunum Leil Sauna Control Unit integration."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING

from pysaunum import SaunumData

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import LeilSaunaConfigEntry
from .entity import LeilSaunaEntity

if TYPE_CHECKING:
from .coordinator import LeilSaunaCoordinator

PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class LeilSaunaBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Leil Sauna binary sensor entity."""

value_fn: Callable[[SaunumData], bool | None]


BINARY_SENSORS: tuple[LeilSaunaBinarySensorEntityDescription, ...] = (
LeilSaunaBinarySensorEntityDescription(
key="door_open",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data.door_open,
),
LeilSaunaBinarySensorEntityDescription(
key="alarm_door_open",
translation_key="alarm_door_open",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.alarm_door_open,
),
LeilSaunaBinarySensorEntityDescription(
key="alarm_door_sensor",
translation_key="alarm_door_sensor",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.alarm_door_sensor,
),
LeilSaunaBinarySensorEntityDescription(
key="alarm_thermal_cutoff",
translation_key="alarm_thermal_cutoff",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.alarm_thermal_cutoff,
),
LeilSaunaBinarySensorEntityDescription(
key="alarm_internal_temp",
translation_key="alarm_internal_temp",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.alarm_internal_temp,
),
LeilSaunaBinarySensorEntityDescription(
key="alarm_temp_sensor_short",
translation_key="alarm_temp_sensor_short",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.alarm_temp_sensor_short,
),
LeilSaunaBinarySensorEntityDescription(
key="alarm_temp_sensor_open",
translation_key="alarm_temp_sensor_open",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.alarm_temp_sensor_open,
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: LeilSaunaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Saunum Leil Sauna binary sensors from a config entry."""
coordinator = entry.runtime_data

async_add_entities(
LeilSaunaBinarySensorEntity(coordinator, description)
for description in BINARY_SENSORS
if description.value_fn(coordinator.data) is not None
)


class LeilSaunaBinarySensorEntity(LeilSaunaEntity, BinarySensorEntity):
"""Representation of a Saunum Leil Sauna binary sensor."""

entity_description: LeilSaunaBinarySensorEntityDescription

def __init__(
self,
coordinator: LeilSaunaCoordinator,
description: LeilSaunaBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}"
self.entity_description = description

@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.coordinator.data)
6 changes: 6 additions & 0 deletions homeassistant/components/saunum/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ def hvac_action(self) -> HVACAction | None:

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new HVAC mode."""
if hvac_mode == HVACMode.HEAT and self.coordinator.data.door_open:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="door_open",
)

try:
if hvac_mode == HVACMode.HEAT:
await self.coordinator.client.async_start_session()
Expand Down
23 changes: 23 additions & 0 deletions homeassistant/components/saunum/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@
}
},
"entity": {
"binary_sensor": {
"alarm_door_open": {
"name": "Door open during heating alarm"
},
"alarm_door_sensor": {
"name": "Door open too long alarm"
},
"alarm_internal_temp": {
"name": "Internal temperature alarm"
},
"alarm_temp_sensor_open": {
"name": "Temperature sensor disconnected alarm"
},
"alarm_temp_sensor_short": {
"name": "Temperature sensor shorted alarm"
},
"alarm_thermal_cutoff": {
"name": "Thermal cutoff alarm"
}
},
"light": {
"light": {
"name": "[%key:component::light::title%]"
Expand All @@ -49,6 +69,9 @@
"communication_error": {
"message": "Communication error: {error}"
},
"door_open": {
"message": "Cannot start sauna session when sauna door is open"
},
"session_not_active": {
"message": "Cannot change fan mode when sauna session is not active"
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/smarla/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmarlaapi", "pysignalr"],
"quality_scale": "bronze",
"requirements": ["pysmarlaapi==0.9.2"]
"requirements": ["pysmarlaapi==0.9.3"]
}
2 changes: 2 additions & 0 deletions homeassistant/components/switchbot_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ async def make_device_data(
"Color Bulb",
"RGBICWW Floor Lamp",
"RGBICWW Strip Light",
"Ceiling Light",
"Ceiling Light Pro",
]:
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id
Expand Down
Loading
Loading