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 homeassistant/components/androidtv_remote/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["androidtvremote2"],
"quality_scale": "platinum",
"requirements": ["androidtvremote2==0.3.1"],
"requirements": ["androidtvremote2==0.3.2"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}
26 changes: 25 additions & 1 deletion homeassistant/components/caldav/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

import caldav

from homeassistant.components.calendar import CalendarEvent, extract_offset
from homeassistant.components.calendar import (
CalendarEvent,
CalendarEventStatus,
extract_offset,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
Expand All @@ -23,6 +27,24 @@
OFFSET = "!!"


def _get_status(vevent: caldav.CalendarObjectResource) -> CalendarEventStatus | None:
"""Return the rfc5545 STATUS of a VEVENT, if a calendar entity reports it.

Anything outside the supported set is dropped rather than passed on, which
covers both the cancelled status a calendar entity does not report and the
iana-tokens and x-names that rfc5545 also permits here: reporting no status
at all is closer to the truth than reporting one the consumer cannot
interpret.
"""
if (value := get_attr_value(vevent, "status")) is None:
return None
try:
return CalendarEventStatus(value.lower())
except ValueError:
_LOGGER.debug("Ignoring unsupported event status %s", value)
return None


class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
"""Class to utilize the calendar dav client object to get next event."""

Expand Down Expand Up @@ -86,6 +108,7 @@ def _get_events(
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
status=_get_status(vevent),
)
)

Expand Down Expand Up @@ -194,6 +217,7 @@ def _get_next_event(
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
status=_get_status(vevent),
)
return next_event, offset

Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/calendar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
LIST_EVENT_FIELDS,
CalendarEntityFeature,
CalendarEntityStateAttribute,
CalendarEventStatus,
)

# mypy: disallow-any-generics
Expand Down Expand Up @@ -379,6 +380,7 @@ class CalendarEvent:
uid: str | None = None
recurrence_id: str | None = None
rrule: str | None = None
status: CalendarEventStatus | None = None

@property
def start_datetime_local(self) -> datetime.datetime:
Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/calendar/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ class CalendarEntityFeature(IntFlag):
UPDATE_EVENT = 4


class CalendarEventStatus(StrEnum):
"""Status of a calendar event.

A subset of the statuses defined by the rfc5545 STATUS property: a calendar
entity does not return cancelled events, so that value is not represented
here.

An event without a status is not the same as a confirmed event: it means
the calendar did not report one, either because the source does not
support it or because the integration does not read it yet.
"""

CONFIRMED = "confirmed"
TENTATIVE = "tentative"


# rfc5545 fields
EVENT_UID = "uid"
EVENT_START = "dtstart"
Expand All @@ -43,6 +59,7 @@ class CalendarEntityFeature(IntFlag):
EVENT_RECURRENCE_ID = "recurrence_id"
EVENT_RECURRENCE_RANGE = "recurrence_range"
EVENT_RRULE = "rrule"
EVENT_STATUS = "status"

# Service call fields
EVENT_START_DATE = "start_date"
Expand All @@ -69,4 +86,5 @@ class CalendarEntityFeature(IntFlag):
EVENT_SUMMARY,
EVENT_DESCRIPTION,
EVENT_LOCATION,
EVENT_STATUS,
}
6 changes: 6 additions & 0 deletions homeassistant/components/cloud/account_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ def domain(self) -> str:
"""Domain that is providing the implementation."""
return DOMAIN

@property
@override
def service_domain(self) -> str:
"""Domain of the service the tokens are for."""
return self.service

@override
async def async_generate_authorize_url(self, flow_id: str) -> str:
"""Generate a url for the user to authorize."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/duco/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"iot_class": "local_polling",
"loggers": ["duco_connectivity"],
"quality_scale": "platinum",
"requirements": ["python-duco-connectivity==0.14.0"],
"requirements": ["python-duco-connectivity==0.15.0"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/flexit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .const import CONF_BAUDRATE, CONF_UNIT, DEFAULT_PORT, TYPE_SERIAL
from .coordinator import FlexitConfigEntry, FlexitDataCoordinator

_PLATFORMS: list[Platform] = [Platform.CLIMATE]
_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE]


def create_modbus_params(
Expand Down
80 changes: 80 additions & 0 deletions homeassistant/components/flexit/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Binary sensor platform for the Flexit integration."""

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

from flexit_modbus import Measurements

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 .coordinator import FlexitConfigEntry, FlexitDataCoordinator
from .entity import FlexitEntity


@dataclass(kw_only=True, frozen=True)
class FlexitBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describe a Flexit binary sensor entity."""

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


BINARY_SENSORS: tuple[FlexitBinarySensorEntityDescription, ...] = (
FlexitBinarySensorEntityDescription(
key="filter_alarm",
translation_key="filter_alarm",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda measurements: measurements.filter_alarm,
),
FlexitBinarySensorEntityDescription(
key="electric_heater_enabled",
translation_key="electric_heater_enabled",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda measurements: measurements.electric_heater_enabled,
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: FlexitConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Flexit binary sensor entities."""
coordinator = entry.runtime_data
async_add_entities(
FlexitBinarySensor(coordinator, description) for description in BINARY_SENSORS
)


class FlexitBinarySensor(FlexitEntity, BinarySensorEntity):
"""Representation of a Flexit binary sensor."""

entity_description: FlexitBinarySensorEntityDescription

def __init__(
self,
coordinator: FlexitDataCoordinator,
entity_description: FlexitBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
assert coordinator.config_entry is not None
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}-{entity_description.key}"
)

@property
@override
def is_on(self) -> bool | None:
"""Return the binary sensor state."""
return self.entity_description.value_fn(self.coordinator.device.measurements)
2 changes: 1 addition & 1 deletion homeassistant/components/flexit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
TYPE_TCP = "tcp"
TYPE_SERIAL = "serial"

DEFAULT_BAUDRATE = 57600
DEFAULT_BAUDRATE = 9600
10 changes: 10 additions & 0 deletions homeassistant/components/flexit/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@
}
}
},
"entity": {
"binary_sensor": {
"electric_heater_enabled": {
"name": "Electric heater enabled"
},
"filter_alarm": {
"name": "Filter alarm"
}
}
},
"issues": {
"deprecated_yaml_no_import": {
"description": "Configuring Flexit using YAML is being removed.\n\nYour existing YAML configuration could not be automatically imported because the Modbus connection details are configured separately, in a `modbus:` hub, which is not accessible from the `climate` platform configuration.\n\nRemove the `flexit` configuration from your configuration.yaml file, then add the integration again from the Home Assistant UI, providing the Modbus connection details and unit ID of your Flexit unit.",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/fronius/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ async def _update_method(self) -> dict[SolarNetId, Any]:
values[f"mppt_{number}_current_dc"] = module.current
values[f"mppt_{number}_voltage_dc"] = module.voltage
values[f"mppt_{number}_power_dc"] = module.power
values[f"mppt_{number}_energy_dc"] = module.energy
values[f"mppt_{number}_energy"] = module.energy
return self._as_device_data(values)


Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/fronius/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,12 @@ def _modbus_mppt_descriptions(
translation_placeholders={"mppt_no": str(mppt_no)},
),
FroniusSensorEntityDescription(
key=f"mppt_{mppt_no}_energy_dc",
key=f"mppt_{mppt_no}_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
invalid_when_falsy=True,
translation_key="modbus_mppt_energy_dc",
translation_key="modbus_mppt_energy",
translation_placeholders={"mppt_no": str(mppt_no)},
),
]
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/fronius/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@
"modbus_mppt_current_dc": {
"name": "MPPT {mppt_no} DC current"
},
"modbus_mppt_energy_dc": {
"name": "MPPT {mppt_no} DC energy"
"modbus_mppt_energy": {
"name": "MPPT {mppt_no} energy"
},
"modbus_mppt_power_dc": {
"name": "MPPT {mppt_no} DC power"
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/google/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
CalendarEntityDescription,
CalendarEntityFeature,
CalendarEvent,
CalendarEventStatus,
extract_offset,
is_offset_reached,
)
Expand Down Expand Up @@ -535,6 +536,11 @@ def _get_calendar_event(event: Event) -> CalendarEvent:
end=event.end.value,
description=event.description,
location=event.location,
# The Google API defaults an omitted status to confirmed, and gcal_sync
# applies that default, so this is never None. It drops cancelled
# events when building the timeline, so only the statuses a calendar
# entity reports reach here, already in lower case.
status=CalendarEventStatus(event.status.value),
)


Expand Down
19 changes: 19 additions & 0 deletions homeassistant/components/local_calendar/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
CalendarEntity,
CalendarEntityFeature,
CalendarEvent,
CalendarEventStatus,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
Expand Down Expand Up @@ -228,6 +229,23 @@ def _parse_event(event: dict[str, Any]) -> Event:
raise vol.Invalid("Error parsing event input fields") from err


def _get_status(event: Event) -> CalendarEventStatus | None:
"""Return the status of an event, if a calendar entity reports that status.

ical models the full rfc5545 set, which includes cancelled, and an imported
calendar can contain such an event. A calendar entity does not report a
cancelled status, so anything outside the supported set maps to no status.
ical's enum is a plain (str, Enum) rather than a StrEnum, so its value has
to be read explicitly.
"""
if event.status is None:
return None
try:
return CalendarEventStatus(event.status.value.lower())
except ValueError:
return None


def _get_calendar_event(event: Event) -> CalendarEvent:
"""Return a CalendarEvent from an API event."""
start: datetime | date
Expand All @@ -252,4 +270,5 @@ def _get_calendar_event(event: Event) -> CalendarEvent:
rrule=event.rrule.as_rrule_str() if event.rrule else None,
recurrence_id=event.recurrence_id,
location=event.location,
status=_get_status(event),
)
Loading
Loading