Skip to content
Closed
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: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion homeassistant/brands/honeywell.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"domain": "honeywell",
"name": "Honeywell",
"integrations": ["lyric", "evohome", "honeywell"]
"integrations": ["lyric", "evohome", "honeywell", "honeywell_string_lights"]
}
24 changes: 19 additions & 5 deletions homeassistant/components/battery/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,33 @@

CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
BATTERY_DOMAIN_SPECS,
STATE_ON,
support_duration=True,
primary_entities_only=False,
),
"is_not_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
BATTERY_DOMAIN_SPECS,
STATE_OFF,
support_duration=True,
primary_entities_only=False,
),
"is_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_ON,
support_duration=True,
primary_entities_only=False,
),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_OFF,
support_duration=True,
primary_entities_only=False,
),
"is_level": make_entity_numerical_condition(
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
BATTERY_PERCENTAGE_DOMAIN_SPECS,
PERCENTAGE,
primary_entities_only=False,
),
}

Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/battery/conditions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
entity:
- domain: binary_sensor
device_class: battery
primary_entities_only: false
fields:
behavior: &condition_behavior
required: true
Expand Down Expand Up @@ -42,6 +43,7 @@ is_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
Expand All @@ -51,6 +53,7 @@ is_not_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
Expand All @@ -60,6 +63,7 @@ is_level:
entity:
- domain: sensor
device_class: battery
primary_entities_only: false
fields:
behavior: *condition_behavior
threshold:
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/esphome/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
MediaPlayerInfo,
MediaPlayerSupportedFormat,
NumberInfo,
RadioFrequencyInfo,
SelectInfo,
SensorInfo,
SensorState,
Expand Down Expand Up @@ -88,6 +89,7 @@
FanInfo: Platform.FAN,
InfraredInfo: Platform.INFRARED,
LightInfo: Platform.LIGHT,
RadioFrequencyInfo: Platform.RADIO_FREQUENCY,
LockInfo: Platform.LOCK,
MediaPlayerInfo: Platform.MEDIA_PLAYER,
NumberInfo: Platform.NUMBER,
Expand Down
77 changes: 77 additions & 0 deletions homeassistant/components/esphome/radio_frequency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Radio Frequency platform for ESPHome."""

from __future__ import annotations

from functools import partial
import logging

from aioesphomeapi import (
EntityState,
RadioFrequencyCapability,
RadioFrequencyInfo,
RadioFrequencyModulation,
)
from rf_protocols import ModulationType, RadioFrequencyCommand

from homeassistant.components.radio_frequency import RadioFrequencyTransmitterEntity
from homeassistant.core import callback

from .entity import (
EsphomeEntity,
convert_api_error_ha_error,
platform_async_setup_entry,
)

_LOGGER = logging.getLogger(__name__)

PARALLEL_UPDATES = 0

MODULATION_TYPE_TO_ESPHOME: dict[ModulationType, RadioFrequencyModulation] = {
ModulationType.OOK: RadioFrequencyModulation.OOK,
}


class EsphomeRadioFrequencyEntity(
EsphomeEntity[RadioFrequencyInfo, EntityState], RadioFrequencyTransmitterEntity
):
"""ESPHome radio frequency entity using native API."""

@property
def supported_frequency_ranges(self) -> list[tuple[int, int]]:
"""Return supported frequency ranges from device info."""
return [(self._static_info.frequency_min, self._static_info.frequency_max)]

@callback
def _on_device_update(self) -> None:
"""Call when device updates or entry data changes."""
super()._on_device_update()
if self._entry_data.available:
self.async_write_ha_state()

@convert_api_error_ha_error
async def async_send_command(self, command: RadioFrequencyCommand) -> None:
"""Send an RF command."""
timings = command.get_raw_timings()
_LOGGER.debug("Sending RF command: %s", timings)

self._client.radio_frequency_transmit_raw_timings(
self._static_info.key,
frequency=command.frequency,
timings=timings,
modulation=MODULATION_TYPE_TO_ESPHOME[command.modulation],
# In ESPHome, repeat_count is total number of times to send the command, while in rf_protocols
# it's the number of additional times to send it, so we need to add 1 here.
repeat_count=command.repeat_count + 1,
device_id=self._static_info.device_id,
)


async_setup_entry = partial(
platform_async_setup_entry,
info_type=RadioFrequencyInfo,
entity_type=EsphomeRadioFrequencyEntity,
state_type=EntityState,
info_filter=lambda info: bool(
info.capabilities & RadioFrequencyCapability.TRANSMITTER
),
)
11 changes: 7 additions & 4 deletions homeassistant/components/flume/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfVolume
from homeassistant.const import UnitOfVolume, UnitOfVolumeFlowRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
Expand All @@ -34,7 +34,8 @@
key="current_interval",
translation_key="current_interval",
suggested_display_precision=2,
native_unit_of_measurement=f"{UnitOfVolume.GALLONS}/m",
native_unit_of_measurement=UnitOfVolumeFlowRate.GALLONS_PER_MINUTE,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
Expand Down Expand Up @@ -65,14 +66,16 @@
key="last_60_min",
translation_key="last_60_min",
suggested_display_precision=2,
native_unit_of_measurement=f"{UnitOfVolume.GALLONS}/h",
native_unit_of_measurement=UnitOfVolumeFlowRate.GALLONS_PER_HOUR,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="last_24_hrs",
translation_key="last_24_hrs",
suggested_display_precision=2,
native_unit_of_measurement=f"{UnitOfVolume.GALLONS}/d",
native_unit_of_measurement=UnitOfVolumeFlowRate.GALLONS_PER_DAY,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
Expand Down
2 changes: 0 additions & 2 deletions homeassistant/components/fritz/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ class MeshRoles(StrEnum):

BUTTON_TYPE_WOL = "WakeOnLan"

UPTIME_DEVIATION = 5

FRITZ_EXCEPTIONS = (
ConnectionError,
FritzActionError,
Expand Down
25 changes: 6 additions & 19 deletions homeassistant/components/fritz/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow

from .const import DSL_CONNECTION, UPTIME_DEVIATION
from .const import DSL_CONNECTION
from .coordinator import FritzConfigEntry
from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
from .models import ConnectionInfo
Expand All @@ -39,31 +39,18 @@
PARALLEL_UPDATES = 0


def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime:
"""Calculate uptime with deviation."""
delta_uptime = utcnow() - timedelta(seconds=seconds_uptime)

if (
not last_value
or abs((delta_uptime - last_value).total_seconds()) > UPTIME_DEVIATION
):
return delta_uptime

return last_value


def _retrieve_device_uptime_state(
status: FritzStatus, last_value: datetime
status: FritzStatus, last_value: datetime | None
) -> datetime:
"""Return uptime from device."""
return _uptime_calculation(status.device_uptime, last_value)
return utcnow() - timedelta(seconds=status.device_uptime)


def _retrieve_connection_uptime_state(
status: FritzStatus, last_value: datetime | None
) -> datetime:
"""Return uptime from connection."""
return _uptime_calculation(status.connection_uptime, last_value)
return utcnow() - timedelta(seconds=status.connection_uptime)


def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str:
Expand Down Expand Up @@ -200,7 +187,7 @@ class FritzDeviceSensorEntityDescription(
FritzConnectionSensorEntityDescription(
key="connection_uptime",
translation_key="connection_uptime",
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=_retrieve_connection_uptime_state,
),
Expand Down Expand Up @@ -308,7 +295,7 @@ class FritzDeviceSensorEntityDescription(
FritzDeviceSensorEntityDescription(
key="device_uptime",
translation_key="device_uptime",
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=_retrieve_device_uptime_state,
),
Expand Down
49 changes: 45 additions & 4 deletions homeassistant/components/hassio/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,27 @@ async def async_setup_entry(


class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
"""Update entity to handle updates for the Supervisor add-ons."""
"""Update entity to handle updates for the Supervisor add-ons.

The ``addon_manager_update`` job emits a ``done=True`` WS event as soon as
Supervisor finishes the container work, a few milliseconds before the
``/store/addons/<slug>/update`` HTTP call returns. If we clear
``_attr_in_progress`` on that event while the coordinator data still
carries the pre-update version, the UI briefly flips back to
"Update available" before ``async_install`` can refresh. ``_update_ongoing``
survives both the WS done event and the base ``UpdateEntity`` reset, so
the installing state remains until the coordinator confirms a new
``installed_version``.
"""

_attr_supported_features = (
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.BACKUP
| UpdateEntityFeature.RELEASE_NOTES
| UpdateEntityFeature.PROGRESS
)
_update_ongoing: bool = False
_version_before_update: str | None = None

@property
def _addon_data(self) -> dict:
Expand All @@ -121,6 +134,13 @@ def installed_version(self) -> str | None:
"""Version installed and in use."""
return self._addon_data[ATTR_VERSION]

@property
def in_progress(self) -> bool | None:
"""Return combined progress from the update job and refresh phase."""
if self._update_ongoing:
return True
return self._attr_in_progress

@property
def entity_picture(self) -> str | None:
"""Return the icon of the add-on if any."""
Expand Down Expand Up @@ -154,13 +174,34 @@ async def async_install(
**kwargs: Any,
) -> None:
"""Install an update."""
self._version_before_update = self.installed_version
self._update_ongoing = True
self._attr_in_progress = True
self.async_write_ha_state()
await update_addon(
self.hass, self._addon_slug, backup, self.title, self.installed_version
)
try:
await update_addon(
self.hass, self._addon_slug, backup, self.title, self.installed_version
)
except HomeAssistantError:
self._update_ongoing = False
self._version_before_update = None
self._attr_in_progress = False
self._attr_update_percentage = None
self.async_write_ha_state()
raise
await self.coordinator.async_refresh()

@callback
def _handle_coordinator_update(self) -> None:
"""Clear the ongoing flag once the installed version has changed."""
if (
self._update_ongoing
and self.installed_version != self._version_before_update
):
self._update_ongoing = False
self._version_before_update = None
super()._handle_coordinator_update()

@callback
def _update_job_changed(self, job: Job) -> None:
"""Process update for this entity's update job."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homeassistant/triggers/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def update_entity_trigger(
elif (
new_state.domain == "sensor"
and new_state.attributes.get(ATTR_DEVICE_CLASS)
== sensor.SensorDeviceClass.TIMESTAMP
in (sensor.SensorDeviceClass.TIMESTAMP, sensor.SensorDeviceClass.UPTIME)
and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
):
trigger_dt = dt_util.parse_datetime(new_state.state)
Expand Down
Loading
Loading