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
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ class ConfClimateFanSpeedMode(StrEnum):
write=False, state_required=True, valid_dpt="9.001"
),
vol.Optional(CONF_GA_HUMIDITY_CURRENT): GASelector(
write=False, valid_dpt="9.002"
write=False, valid_dpt="9.007"
),
vol.Required(CONF_TARGET_TEMPERATURE): GroupSelect(
GroupSelectOption(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/kodi/browse_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ async def library_payload(hass):
for child in library_info.children:
child.thumbnail = "https://brands.home-assistant.io/_/kodi/logo.png"

with contextlib.suppress(media_source.BrowseError):
with contextlib.suppress(BrowseError):
item = await media_source.async_browse_media(
hass, None, content_filter=media_source_content_filter
)
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/miele/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"energy_forecast": {
"default": "mdi:lightning-bolt-outline"
},
"finish": {
"default": "mdi:clock-end"
},
"plate": {
"default": "mdi:circle-outline",
"state": {
Expand Down Expand Up @@ -83,6 +86,9 @@
"spin_speed": {
"default": "mdi:sync"
},
"start": {
"default": "mdi:clock-start"
},
"start_time": {
"default": "mdi:clock-start"
},
Expand Down
147 changes: 140 additions & 7 deletions homeassistant/components/miele/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any, Final, cast

Expand All @@ -29,6 +30,7 @@
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util

from .const import (
COFFEE_SYSTEM_PROFILE,
Expand Down Expand Up @@ -102,12 +104,47 @@ def _get_coffee_profile(value: MieleDevice) -> str | None:
return None


def _convert_start_timestamp(
elapsed_time_list: list[int], start_time_list: list[int]
) -> datetime | None:
"""Convert raw values representing time into start timestamp."""
now = dt_util.utcnow()
elapsed_duration = _convert_duration(elapsed_time_list)
delayed_start_duration = _convert_duration(start_time_list)
if (elapsed_duration is None or elapsed_duration == 0) and (
delayed_start_duration is None or delayed_start_duration == 0
):
return None
if elapsed_duration is not None and elapsed_duration > 0:
duration = -elapsed_duration
elif delayed_start_duration is not None and delayed_start_duration > 0:
duration = delayed_start_duration
delta = timedelta(minutes=duration)
return (now + delta).replace(second=0, microsecond=0)


def _convert_finish_timestamp(
remaining_time_list: list[int], start_time_list: list[int]
) -> datetime | None:
"""Convert raw values representing time into finish timestamp."""
now = dt_util.utcnow()
program_duration = _convert_duration(remaining_time_list)
delayed_start_duration = _convert_duration(start_time_list)
if program_duration is None or program_duration == 0:
return None
duration = program_duration + (
delayed_start_duration if delayed_start_duration is not None else 0
)
delta = timedelta(minutes=duration)
return (now + delta).replace(second=0, microsecond=0)


@dataclass(frozen=True, kw_only=True)
class MieleSensorDescription(SensorEntityDescription):
"""Class describing Miele sensor entities."""

value_fn: Callable[[MieleDevice], StateType]
end_value_fn: Callable[[StateType], StateType] | None = None
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
unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None
Expand Down Expand Up @@ -428,6 +465,60 @@ class MieleSensorDefinition:
suggested_unit_of_measurement=UnitOfTime.HOURS,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_finish_timestamp",
translation_key="finish",
value_fn=lambda value: _convert_finish_timestamp(
value.state_remaining_time, value.state_start_time
),
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_start_timestamp",
translation_key="start",
value_fn=lambda value: _convert_start_timestamp(
value.state_elapsed_time, value.state_start_time
),
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
Expand Down Expand Up @@ -620,6 +711,8 @@ def _get_entity_class(definition: MieleSensorDefinition) -> type[MieleSensor]:
"state_elapsed_time": MieleTimeSensor,
"state_remaining_time": MieleTimeSensor,
"state_start_time": MieleTimeSensor,
"state_start_timestamp": MieleAbsoluteTimeSensor,
"state_finish_timestamp": MieleAbsoluteTimeSensor,
"current_energy_consumption": MieleConsumptionSensor,
"current_water_consumption": MieleConsumptionSensor,
}.get(definition.description.key, MieleSensor)
Expand Down Expand Up @@ -743,7 +836,7 @@ def __init__(
self._attr_unique_id = description.unique_id_fn(device_id, description)

@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.device)

Expand All @@ -761,7 +854,7 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
class MieleRestorableSensor(MieleSensor, RestoreSensor):
"""Representation of a Sensor whose internal state can be restored."""

_attr_native_value: StateType
_attr_native_value: StateType | datetime

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
Expand All @@ -773,7 +866,7 @@ async def async_added_to_hass(self) -> None:
self._attr_native_value = last_data.native_value # type: ignore[assignment]

@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor.

It is necessary to override `native_value` to fall back to the default
Expand Down Expand Up @@ -934,6 +1027,40 @@ def _update_native_value(self) -> None:
self._attr_native_value = current_value


class MieleAbsoluteTimeSensor(MieleRestorableSensor):
"""Representation of absolute time sensors handling precision correctness."""

_previous_value: StateType | datetime = None

def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)

# The API reports with minute precision, to avoid changing
# the value too often, we keep the cached value if it differs
# less than 90s from the new value
if (
isinstance(self._previous_value, datetime)
and isinstance(current_value, datetime)
and (
self._previous_value - timedelta(seconds=90)
< current_value
< self._previous_value + timedelta(seconds=90)
)
) or current_status == StateStatus.PROGRAM_ENDED:
return

# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
if current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
self._attr_native_value = None

# otherwise, cache value and return it
else:
self._attr_native_value = current_value
self._previous_value = current_value


class MieleConsumptionSensor(MieleRestorableSensor):
"""Representation of consumption sensors keeping state from cache."""

Expand All @@ -943,13 +1070,19 @@ def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
# Guard for corrupt restored value
restored_value = (
self._attr_native_value
if isinstance(self._attr_native_value, (int, float))
else 0
)
last_value = (
float(cast(str, self._attr_native_value))
float(cast(str, restored_value))
if self._attr_native_value is not None
else 0
)

# force unknown when appliance is not able to report consumption
# Force unknown when appliance is not able to report consumption
if current_status in (
StateStatus.ON,
StateStatus.OFF,
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/miele/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@
"energy_forecast": {
"name": "Energy forecast"
},
"finish": {
"name": "Finish"
},
"plate": {
"name": "Plate {plate_no}",
"state": {
Expand Down Expand Up @@ -1015,6 +1018,9 @@
"spin_speed": {
"name": "Spin speed"
},
"start": {
"name": "Start"
},
"start_time": {
"name": "Start in"
},
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/noaa_tides/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Constants for the NOAA Tides integration."""

from datetime import timedelta

CONF_STATION_ID = "station_id"

DEFAULT_NAME = "NOAA Tides"
DEFAULT_PREDICTION_LENGTH = timedelta(days=2)
DEFAULT_TIMEZONE = "lst_ldt"

ATTRIBUTION = "Data provided by NOAA"
29 changes: 14 additions & 15 deletions homeassistant/components/noaa_tides/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from datetime import datetime, timedelta
from datetime import datetime
import logging
from typing import TYPE_CHECKING, Any, Literal, TypedDict

Expand All @@ -22,20 +22,20 @@
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.unit_system import METRIC_SYSTEM

from .const import (
ATTRIBUTION,
CONF_STATION_ID,
DEFAULT_NAME,
DEFAULT_PREDICTION_LENGTH,
DEFAULT_TIMEZONE,
)
from .helpers import get_station_unique_id

if TYPE_CHECKING:
from pandas import Timestamp

_LOGGER = logging.getLogger(__name__)

CONF_STATION_ID = "station_id"

DEFAULT_NAME = "NOAA Tides"
DEFAULT_TIMEZONE = "lst_ldt"

SCAN_INTERVAL = timedelta(minutes=60)

TIMEZONES = ["gmt", "lst", "lst_ldt"]
UNIT_SYSTEMS = ["english", "metric"]

Expand Down Expand Up @@ -63,9 +63,9 @@ def setup_platform(
if CONF_UNIT_SYSTEM in config:
unit_system = config[CONF_UNIT_SYSTEM]
elif hass.config.units is METRIC_SYSTEM:
unit_system = UNIT_SYSTEMS[1]
unit_system = "metric"
else:
unit_system = UNIT_SYSTEMS[0]
unit_system = "english"

try:
station = coops.Station(station_id, unit_system)
Expand Down Expand Up @@ -97,7 +97,7 @@ class NOAATidesData(TypedDict):
class NOAATidesAndCurrentsSensor(SensorEntity):
"""Representation of a NOAA Tides and Currents sensor."""

_attr_attribution = "Data provided by NOAA"
_attr_attribution = ATTRIBUTION

def __init__(self, name, station_id, timezone, unit_system, station) -> None:
"""Initialize the sensor."""
Expand Down Expand Up @@ -141,8 +141,8 @@ def extra_state_attributes(self) -> dict[str, Any]:
return attr

@property
def native_value(self):
"""Return the state of the device."""
def native_value(self) -> str | None:
"""Return the state."""
if self.data is None:
return None
api_time = self.data["time_stamp"][0]
Expand All @@ -157,8 +157,7 @@ def native_value(self):
def update(self) -> None:
"""Get the latest data from NOAA Tides and Currents API."""
begin = datetime.now()
delta = timedelta(days=2)
end = begin + delta
end = begin + DEFAULT_PREDICTION_LENGTH
try:
df_predictions = self._station.get_data(
begin_date=begin.strftime("%Y%m%d %H:%M"),
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/overkiz/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,20 @@ class OverkizSensorDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
OverkizSensorDescription(
key=OverkizState.IO_POWER_HEAT_PUMP,
name="Heat pump power consumption",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
OverkizSensorDescription(
key=OverkizState.IO_POWER_HEAT_ELECTRICAL,
name="Electric power consumption",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
OverkizSensorDescription(
key=OverkizState.CORE_CONSUMPTION_TARIFF1,
name="Consumption tariff 1",
Expand Down
Loading
Loading