Compare commits

..

1 Commits

Author SHA1 Message Date
Jan Čermák 9aa9278eec Bump base image to 2026.02.0 with Python 3.14.3, use 3.14.3 in CI
This also bumps libcec used in the base image to 7.1.1, full changelog:
* https://github.com/home-assistant/docker/releases/tag/2026.02.0

Python changelog:
* https://docs.python.org/release/3.14.3/whatsnew/changelog.html
2026-04-29 09:10:20 +02:00
298 changed files with 4286 additions and 11326 deletions
@@ -15,6 +15,7 @@ description: Everything you need to know to build, test and review Home Assistan
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
- "potato" is a forbidden word for an integration and should never be used.
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
@@ -18,6 +18,7 @@ excludeAgent: "cloud-agent"
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
- "potato" is a forbidden word for an integration and should never be used.
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
+2 -3
View File
@@ -6,7 +6,7 @@
"pep621",
"pip_requirements",
"pre-commit",
"custom.regex",
"regex",
"homeassistant-manifest"
],
@@ -27,9 +27,8 @@
]
},
"customManagers": [
"regexManagers": [
{
"customType": "regex",
"description": "Update ruff required-version in pyproject.toml",
"managerFilePatterns": ["/^pyproject\\.toml$/"],
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
+1 -1
View File
@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.01.0"
BASE_IMAGE_VERSION: "2026.02.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
+3 -29
View File
@@ -366,7 +366,7 @@ jobs:
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
key: >-
@@ -374,8 +374,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
id: cache-uv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -399,7 +398,6 @@ jobs:
if: |
steps.cache-venv.outputs.cache-hit != 'true'
|| steps.cache-apt-check.outputs.cache-hit != 'true'
id: install-os-deps
timeout-minutes: 10
env:
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
@@ -433,10 +431,7 @@ jobs:
sudo chmod -R 755 ${APT_CACHE_BASE}
fi
- name: Save apt cache
if: |
always()
&& steps.cache-apt-check.outputs.cache-hit != 'true'
&& steps.install-os-deps.outcome == 'success'
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
@@ -446,7 +441,6 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
id: create-venv
run: |
python -m venv venv
. venv/bin/activate
@@ -477,26 +471,6 @@ jobs:
- name: Check dirty
run: |
./script/check_dirty
- name: Save uv wheel cache
if: |
(success() && steps.cache-venv.outputs.cache-hit != 'true')
|| (always()
&& steps.create-venv.outcome == 'success'
&& steps.cache-uv.outputs.cache-matched-key == '')
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-uv-key.outputs.key }}
- name: Save base Python virtual environment
if: always() && steps.create-venv.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
hassfest:
name: Check hassfest
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
rev: v0.15.10
hooks:
- id: ruff-check
args:
+1 -1
View File
@@ -1 +1 @@
3.14.2
3.14.3
Generated
-2
View File
@@ -1241,8 +1241,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont
/homeassistant/components/omie/ @luuuis
/tests/components/omie/ @luuuis
/homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core
/homeassistant/components/ondilo_ico/ @JeromeHXP
+1 -16
View File
@@ -2,8 +2,7 @@
from __future__ import annotations
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING
from collections.abc import Callable
import voluptuous as vol
@@ -14,9 +13,6 @@ from .models import PermissionLookup
from .types import PolicyType
from .util import test_all
if TYPE_CHECKING:
from ..models import User
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
__all__ = [
@@ -26,21 +22,10 @@ __all__ = [
"PermissionLookup",
"PolicyPermissions",
"PolicyType",
"filter_entity_ids_by_permission",
"merge_policies",
]
def filter_entity_ids_by_permission(
user: User, entity_ids: Iterable[str], key: str
) -> list[str]:
"""Filter entity IDs to those the user can access for the given policy key."""
if user.is_admin or user.permissions.access_all_entities(key):
return list(entity_ids)
check_entity = user.permissions.check_entity
return [entity_id for entity_id in entity_ids if check_entity(entity_id, key)]
class AbstractPermissions:
"""Default permissions class."""
+1 -1
View File
@@ -143,4 +143,4 @@ class AcaiaRestoreSensor(AcaiaEntity, RestoreSensor):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available or self.native_value is not None
return super().available or self._restored_data is not None
@@ -4,8 +4,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
# --- Unit lists for multi-unit pollutants ---
@@ -237,6 +237,21 @@
"name": "Volatile organic compounds value"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Air Quality",
"triggers": {
"co2_changed": {
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
@@ -7,8 +7,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
.condition_common_for: &condition_common_for
target: *condition_common_target
@@ -160,6 +160,21 @@
"message": "Arming requires a code but none was given for {entity_id}."
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"alarm_arm_away": {
"description": "Arms an alarm in the away mode.",
@@ -7,8 +7,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
options:
- first
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
@@ -11,7 +11,6 @@ from .services import async_setup_services
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
@@ -1,55 +0,0 @@
"""Support for buttons."""
from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import slugify
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonServiceEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up button entities for Alexa Devices."""
coordinator = entry.runtime_data
known_routines: set[str] = set()
def _check_routines() -> None:
current_routines = set(coordinator.api.routines)
new_routines = current_routines - known_routines
if new_routines:
known_routines.update(new_routines)
async_add_entities(
AmazonRoutineButton(coordinator, routine) for routine in new_routines
)
_check_routines()
entry.async_on_unload(coordinator.async_add_listener(_check_routines))
class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
"""Button entity for Alexa routine."""
_attr_has_entity_name = True
def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None:
"""Initialize the routine button entity."""
self._coordinator = coordinator
self._routine = routine
super().__init__(
coordinator,
EntityDescription(key=slugify(routine), name=routine),
)
async def async_press(self) -> None:
"""Handle button press action."""
await self._coordinator.api.call_routine(self._routine)
@@ -12,13 +12,12 @@ from aioamazondevices.structures import AmazonDevice
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import slugify
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
@@ -65,13 +64,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
for identifier_domain, identifier in device.identifiers
if identifier_domain == DOMAIN
}
self.previous_routines: set[str] = {
routine.unique_id
for routine in er.async_entries_for_config_entry(
er.async_get(hass), entry.entry_id
)
if routine.domain == Platform.BUTTON
}
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
@@ -100,13 +92,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
current_devices = set(data.keys())
if stale_devices := self.previous_devices - current_devices:
await self._async_remove_device_stale(stale_devices)
self.previous_devices = current_devices
current_routines = {slugify(routine) for routine in self.api.routines}
if stale_routines := self.previous_routines - current_routines:
await self._async_remove_routine_stale(stale_routines)
self.previous_routines = current_routines
return data
async def _async_remove_device_stale(
@@ -129,23 +116,3 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
async def _async_remove_routine_stale(
self,
stale_routines: set[str],
) -> None:
"""Remove stale routine."""
entity_registry = er.async_get(self.hass)
for routine in stale_routines:
_LOGGER.debug(
"Detected change in routines: routine %s removed",
routine,
)
entity_id = entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
)
if entity_id:
entity_registry.async_remove(entity_id)
@@ -2,10 +2,9 @@
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import DOMAIN
from .coordinator import AmazonDevicesCoordinator
@@ -51,32 +50,3 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
and self._serial_num in self.coordinator.data
and self.device.online
)
class AmazonServiceEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines Alexa Devices entity for service device."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AmazonDevicesCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the service entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, service_device_id(coordinator))},
manufacturer="Amazon",
entry_type=DeviceEntryType.SERVICE,
)
self.entity_description = description
self._attr_unique_id = (
f"{slugify(coordinator.config_entry.unique_id)}-{description.key}"
)
def service_device_id(coordinator: AmazonDevicesCoordinator) -> str:
"""Return service device id."""
return slugify(f"{coordinator.config_entry.unique_id}_service_device")
@@ -7,8 +7,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -72,6 +72,19 @@
"id": "Answer ID",
"sentences": "Sentences"
}
},
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
@@ -7,8 +7,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
options:
- first
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
@@ -194,7 +194,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"switch",
"temperature",
"text",
"timer",
"todo",
"update",
"vacuum",
@@ -902,10 +901,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Remove listeners when removing automation from Home Assistant."""
await super().async_will_remove_from_hass()
await self._async_disable()
if self.registry_entry and self.registry_entry.entity_id != self.entity_id:
# Entity ID change, do not unload the script or conditions as they will
# be reused.
return
self.action_script.async_unload()
if self._condition is not None:
self._condition.async_unload()
+1 -7
View File
@@ -18,10 +18,4 @@ DEFAULT_STREAM_PROFILE = "No stream profile"
DEFAULT_TRIGGER_TIME = 0
DEFAULT_VIDEO_SOURCE = "No video source"
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.SWITCH,
]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.LIGHT, Platform.SWITCH]
-62
View File
@@ -1,62 +0,0 @@
"""Support for Axis event entities."""
from __future__ import annotations
from dataclasses import dataclass
from axis.models.event import Event, EventTopic
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AxisConfigEntry
from .entity import AxisEventDescription, AxisEventEntity
DOORBELL_CONFIG = ("I8116-E", "0")
@dataclass(frozen=True, kw_only=True)
class AxisEventPlatformDescription(AxisEventDescription, EventEntityDescription):
"""Axis event entity description."""
ENTITY_DESCRIPTIONS = (
AxisEventPlatformDescription(
key="Doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=[DoorbellEventType.RING],
event_topic=EventTopic.PORT_INPUT,
name_fn=lambda _hub, _event: "Doorbell",
supported_fn=lambda hub, event: (hub.config.model, event.id) == DOORBELL_CONFIG,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AxisConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up an Axis event platform."""
config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisEvent, ENTITY_DESCRIPTIONS
)
class AxisEvent(AxisEventEntity, EventEntity):
"""Representation of an Axis event entity."""
entity_description: AxisEventPlatformDescription
@callback
def async_event_callback(self, event: Event) -> None:
"""Handle Axis event updates."""
if event.is_tripped:
self._trigger_event(DoorbellEventType.RING)
self.async_write_ha_state()
+5 -19
View File
@@ -30,33 +30,19 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS,
STATE_ON,
support_duration=True,
primary_entities_only=False,
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS,
STATE_OFF,
support_duration=True,
primary_entities_only=False,
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_ON,
support_duration=True,
primary_entities_only=False,
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_OFF,
support_duration=True,
primary_entities_only=False,
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_level": make_entity_numerical_condition(
BATTERY_PERCENTAGE_DOMAIN_SPECS,
PERCENTAGE,
primary_entities_only=False,
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
),
}
@@ -3,14 +3,16 @@
entity:
- domain: binary_sensor
device_class: battery
primary_entities_only: false
fields:
behavior: &condition_behavior
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for: &condition_for
required: true
default: 00:00:00
@@ -40,7 +42,6 @@ is_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
@@ -50,7 +51,6 @@ is_not_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
@@ -60,7 +60,6 @@ is_level:
entity:
- domain: sensor
device_class: battery
primary_entities_only: false
fields:
behavior: *condition_behavior
threshold:
@@ -69,6 +69,21 @@
"name": "Battery is not low"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Battery",
"triggers": {
"level_changed": {
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
@@ -33,13 +33,11 @@ from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import (
SOURCE_USER,
ConfigEntry,
ConfigFlowResult,
ConfigSubentry,
ConfigSubentryData,
ConfigSubentryFlow,
FlowType,
SubentryFlowContext,
SubentryFlowResult,
)
from homeassistant.const import (
@@ -64,6 +62,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
from .binary_sensor import above_greater_than_below, no_overlapping
from .const import (
CONF_OBSERVATIONS,
CONF_P_GIVEN_F,
CONF_P_GIVEN_T,
CONF_PRIOR,
@@ -374,6 +373,26 @@ def _validate_observation_subentry(
return user_input
async def _validate_subentry_from_config_entry(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
# Standard behavior is to merge the result with the options.
# In this case, we want to add a subentry so we update the options directly.
observations: list[dict[str, Any]] = handler.options.setdefault(
CONF_OBSERVATIONS, []
)
if handler.parent_handler.cur_step is not None:
user_input[CONF_PLATFORM] = handler.parent_handler.cur_step["step_id"]
user_input = _validate_observation_subentry(
user_input[CONF_PLATFORM],
user_input,
other_subentries=handler.options[CONF_OBSERVATIONS],
)
observations.append(user_input)
return {}
async def _get_description_placeholders(
handler: SchemaCommonFlowHandler,
) -> dict[str, str]:
@@ -401,12 +420,48 @@ async def _get_description_placeholders(
}
async def _get_observation_menu_options(handler: SchemaCommonFlowHandler) -> list[str]:
"""Return the menu options for the observation selector."""
options = [typ.value for typ in ObservationTypes]
if handler.options.get(CONF_OBSERVATIONS):
options.append("finish")
return options
CONFIG_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
str(USER): SchemaFlowFormStep(
CONFIG_SCHEMA,
validate_user_input=_validate_user,
next_step=str(OBSERVATION_SELECTOR),
description_placeholders=_get_description_placeholders,
)
),
str(OBSERVATION_SELECTOR): SchemaFlowMenuStep(
_get_observation_menu_options,
),
str(ObservationTypes.STATE): SchemaFlowFormStep(
STATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
# Prevent the name of the bayesian sensor from being used as the suggested
# name of the observations
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
str(ObservationTypes.NUMERIC_STATE): SchemaFlowFormStep(
NUMERIC_STATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
str(ObservationTypes.TEMPLATE): SchemaFlowFormStep(
TEMPLATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
"finish": SchemaFlowFormStep(),
}
@@ -442,17 +497,27 @@ class BayesianConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
name: str = options[CONF_NAME]
return name
async def async_on_create_entry(self, result: ConfigFlowResult) -> ConfigFlowResult:
"""Start subentry flow when config entry has been created."""
subentry_result = await self.hass.config_entries.subentries.async_init(
(result["result"].entry_id, "observation"),
context=SubentryFlowContext(source=SOURCE_USER),
)
result["next_flow"] = (
FlowType.CONFIG_SUBENTRIES_FLOW,
subentry_result["flow_id"],
)
return result
@callback
def async_create_entry(
self,
data: Mapping[str, Any],
**kwargs: Any,
) -> ConfigFlowResult:
"""Finish config flow and create a config entry."""
data = dict(data)
observations = data.pop(CONF_OBSERVATIONS)
subentries: list[ConfigSubentryData] = [
ConfigSubentryData(
data=observation,
title=observation[CONF_NAME],
subentry_type="observation",
unique_id=None,
)
for observation in observations
]
self.async_config_flow_finished(data)
return super().async_create_entry(data=data, subentries=subentries, **kwargs)
class ObservationSubentryFlowHandler(ConfigSubentryFlow):
@@ -58,7 +58,6 @@ from .api import (
async_address_present,
async_ble_device_from_address,
async_clear_address_from_match_history,
async_clear_advertisement_history,
async_current_scanners,
async_discovered_service_info,
async_get_advertisement_callback,
@@ -117,7 +116,6 @@ __all__ = [
"async_address_present",
"async_ble_device_from_address",
"async_clear_address_from_match_history",
"async_clear_advertisement_history",
"async_current_scanners",
"async_discovered_service_info",
"async_get_advertisement_callback",
-13
View File
@@ -207,19 +207,6 @@ def async_clear_address_from_match_history(hass: HomeAssistant, address: str) ->
_get_manager(hass).async_clear_address_from_match_history(address)
@hass_callback
def async_clear_advertisement_history(hass: HomeAssistant, address: str) -> None:
"""Clear cached advertisement history for a device.
Causes the next advertisement from this address to be treated as new
data, bypassing the change-detection guard in the Bluetooth manager.
Intended for devices that emit static advertisements as a wake-up
signal, for example, devices that require an active GATT connection
to read sensor data and whose advertisement payload never changes.
"""
_get_manager(hass).async_clear_advertisement_history(address)
@hass_callback
def async_register_scanner(
hass: HomeAssistant,
+2 -1
View File
@@ -293,8 +293,9 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="uptime",
translation_key="last_restart",
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.UPTIME,
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda data: data.uptime,
),
@@ -151,6 +151,9 @@
"laser_remaining_life": {
"name": "Laser remaining lifetime"
},
"last_restart": {
"name": "Last restart"
},
"magenta_drum_page_counter": {
"name": "Magenta drum page counter",
"unit_of_measurement": "[%key:component::brother::entity::sensor::page_counter::unit_of_measurement%]"
@@ -7,8 +7,11 @@ is_event_active:
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -64,6 +64,12 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_offset_type": {
"options": {
"after": "After",
@@ -7,8 +7,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
@@ -271,6 +271,21 @@
"message": "Provided temperature {check_temp} is not valid. Accepted range is {min_temp} to {max_temp}."
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"set_fan_mode": {
"description": "Sets the fan mode of a thermostat.",
@@ -7,8 +7,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
@@ -7,8 +7,11 @@ is_value:
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
threshold:
required: true
selector:
@@ -43,6 +43,21 @@
}
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"decrement": {
"description": "Decrements a counter by its step size.",
@@ -7,8 +7,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for:
required: true
default: 00:00:00
@@ -3,8 +3,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -210,6 +210,21 @@
"name": "Window"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"close_cover": {
"description": "Closes a cover.",
+6 -2
View File
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for:
required: true
default: 00:00:00
@@ -7,7 +7,6 @@ from typing import Any
from devolo_plc_api import Device
from devolo_plc_api.exceptions.device import DeviceNotFound
from yarl import URL
from homeassistant.components import zeroconf
from homeassistant.const import (
@@ -18,7 +17,6 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
from .const import (
@@ -125,25 +123,6 @@ async def async_setup_entry(
entry.runtime_data.coordinators = coordinators
# Ensure the device exists before forwarding to platforms, so that the
# device tracker (which looks up the device by wifi station MAC) is not
# racing the other platforms that create the device via DeviceInfo.
device_info = dr.DeviceInfo(
configuration_url=URL.build(scheme="http", host=device.ip),
identifiers={(DOMAIN, str(device.serial_number))},
manufacturer="devolo",
model=device.product,
model_id=device.mt_number,
serial_number=device.serial_number,
sw_version=device.firmware_version,
)
if device.mac:
device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, device.mac)}
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
**device_info,
)
await hass.config_entries.async_forward_entry_setups(entry, platforms(device))
entry.async_on_unload(
@@ -117,7 +117,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any, Any]] = {
key=LAST_RESTART,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.UPTIME,
device_class=SensorDeviceClass.TIMESTAMP,
value_func=_last_restart,
),
}
@@ -75,6 +75,9 @@
"connected_wifi_clients": {
"name": "Connected Wi-Fi clients"
},
"last_restart": {
"name": "Last restart of the device"
},
"neighboring_wifi_networks": {
"name": "Neighboring Wi-Fi networks"
},
@@ -3,8 +3,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -31,6 +31,21 @@
"name": "Door is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Door",
"triggers": {
"closed": {
+6 -2
View File
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for:
required: true
default: 00:00:00
+1 -1
View File
@@ -6,4 +6,4 @@ from homeassistant.const import Platform
DOMAIN = "duco"
PLATFORMS = [Platform.FAN, Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=30)
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==18.2.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==18.1.0"]
}
+5 -6
View File
@@ -715,9 +715,6 @@ class EnergyPowerSensor(SensorEntity):
self._attr_native_value = None
return
self._attr_native_unit_of_measurement = source_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT
)
self._attr_native_value = value * -1
elif self._is_combined:
@@ -766,11 +763,13 @@ class EnergyPowerSensor(SensorEntity):
# Check first sensor
if source_entry := entity_reg.async_get(self._source_sensors[0]):
device_id = source_entry.device_id
# Combined mode always emits Watts because we convert
# heterogeneous source units internally. For inverted mode the
# unit is copied from the source state in _update_state.
# For combined mode, always use Watts because we may have different source units; for inverted mode, copy source unit
if self._is_combined:
self._attr_native_unit_of_measurement = UnitOfPower.WATT
else:
self._attr_native_unit_of_measurement = (
source_entry.unit_of_measurement
)
# Get source name from registry
source_name = source_entry.name or source_entry.original_name
# Assign power sensor to same device as source sensor(s)
+5 -2
View File
@@ -7,8 +7,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
+13
View File
@@ -93,11 +93,24 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"direction": {
"options": {
"forward": "Forward",
"reverse": "Reverse"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
+6 -2
View File
@@ -7,8 +7,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
options:
- first
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
@@ -695,7 +695,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
_LOGGER.debug("Device tracker cleanup triggered")
device_hosts = {self.mac: Device(True, "", "", "", "", None)}
if self.device_discovery_enabled:
device_hosts.update(await self._async_update_hosts_info())
device_hosts = await self._async_update_hosts_info()
entity_reg: er.EntityRegistry = er.async_get(self.hass)
config_entry = self.config_entry
+1
View File
@@ -294,6 +294,7 @@ CONNECTION_SENSOR_TYPES: tuple[FritzConnectionSensorEntityDescription, ...] = (
DEVICE_SENSOR_TYPES: tuple[FritzDeviceSensorEntityDescription, ...] = (
FritzDeviceSensorEntityDescription(
key="device_uptime",
translation_key="device_uptime",
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=_retrieve_device_uptime_state,
@@ -120,6 +120,9 @@
"cpu_temperature": {
"name": "CPU temperature"
},
"device_uptime": {
"name": "Last restart"
},
"external_ip": {
"name": "External IP"
},
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260429.0"]
"requirements": ["home-assistant-frontend==20260325.8"]
}
@@ -11,7 +11,6 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
]
@@ -1,21 +0,0 @@
"""Diagnostics support for Fumis."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .coordinator import FumisConfigEntry
TO_REDACT_UNIT = {"id", "ip"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: FumisConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = await entry.runtime_data.client.raw_status()
data["unit"] = async_redact_data(data["unit"], TO_REDACT_UNIT)
return data
-51
View File
@@ -5,64 +5,13 @@
"default": "mdi:clock-sync"
}
},
"number": {
"fan_speed": {
"default": "mdi:fan"
},
"power_level": {
"default": "mdi:fire"
}
},
"sensor": {
"alert": {
"default": "mdi:alert",
"state": {
"airflow_malfunction": "mdi:fan-off",
"door_open": "mdi:door-open",
"flue_gas_warning": "mdi:thermometer-alert",
"low_battery": "mdi:battery-alert",
"low_fuel": "mdi:gauge-empty",
"none": "mdi:check-circle",
"service_due": "mdi:wrench-clock",
"speed_sensor_failure": "mdi:fan-alert"
}
},
"combustion_chamber_temperature": {
"default": "mdi:thermometer-high"
},
"detailed_stove_status": {
"default": "mdi:fireplace"
},
"error": {
"default": "mdi:alert-circle",
"state": {
"chimney_alarm": "mdi:broom",
"chimney_dirty": "mdi:broom",
"door_alarm": "mdi:door-open",
"fire_error": "mdi:fire-alert",
"flue_gas_overtemp": "mdi:thermometer-high",
"fuel_ignition_timeout": "mdi:fire-off",
"gas_alarm": "mdi:alert-circle",
"general_error": "mdi:alert-circle",
"grate_error": "mdi:alert-circle",
"ignition_failed": "mdi:fire-alert",
"mfdoor_alarm": "mdi:door-open",
"no_pellet_alarm": "mdi:gauge-empty",
"none": "mdi:check-circle",
"ntc1_alarm": "mdi:thermometer-alert",
"ntc2_alarm": "mdi:thermometer-alert",
"ntc3_alarm": "mdi:thermometer-alert",
"pressure_alarm": "mdi:gauge-empty",
"pressure_sensor_off": "mdi:gauge-empty",
"safety_switch": "mdi:shield-alert",
"sensor_t01_t02": "mdi:thermometer-alert",
"sensor_t01_t03": "mdi:thermometer-alert",
"sensor_t02": "mdi:thermometer-alert",
"sensor_t03_t05": "mdi:thermometer-alert",
"sensor_t04": "mdi:thermometer-alert",
"tc1_alarm": "mdi:thermometer-alert"
}
},
"fan_1_speed": {
"default": "mdi:fan"
},
+1 -1
View File
@@ -12,6 +12,6 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["fumis"],
"quality_scale": "platinum",
"quality_scale": "bronze",
"requirements": ["fumis==0.4.0"]
}
-97
View File
@@ -1,97 +0,0 @@
"""Support for Fumis number entities."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from fumis import Fumis, FumisInfo
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import FumisConfigEntry, FumisDataUpdateCoordinator
from .entity import FumisEntity
from .helpers import fumis_exception_handler
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class FumisNumberEntityDescription(NumberEntityDescription):
"""Describes a Fumis number entity."""
has_fn: Callable[[FumisInfo], bool] = lambda _: True
value_fn: Callable[[FumisInfo], float | None]
set_fn: Callable[[Fumis, float], Awaitable[Any]]
NUMBERS: tuple[FumisNumberEntityDescription, ...] = (
FumisNumberEntityDescription(
key="fan_speed",
translation_key="fan_speed",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=0,
native_max_value=5,
native_step=1,
has_fn=lambda data: len(data.controller.fans) > 0,
value_fn=lambda data: (
data.controller.fans[0].speed if data.controller.fans else None
),
set_fn=lambda client, value: client.set_fan_speed(int(value)),
),
FumisNumberEntityDescription(
key="power_level",
translation_key="power_level",
native_min_value=1,
native_max_value=5,
native_step=1,
value_fn=lambda data: data.controller.power.set_power,
set_fn=lambda client, value: client.set_power(int(value)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FumisConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fumis number entities based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
FumisNumberEntity(coordinator=coordinator, description=description)
for description in NUMBERS
if description.has_fn(coordinator.data)
)
class FumisNumberEntity(FumisEntity, NumberEntity):
"""Defines a Fumis number entity."""
entity_description: FumisNumberEntityDescription
def __init__(
self,
coordinator: FumisDataUpdateCoordinator,
description: FumisNumberEntityDescription,
) -> None:
"""Initialize the Fumis number entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
@property
def native_value(self) -> float | None:
"""Return the current value."""
return self.entity_description.value_fn(self.coordinator.data)
@fumis_exception_handler
async def async_set_native_value(self, value: float) -> None:
"""Set a new value."""
await self.entity_description.set_fn(self.coordinator.client, value)
await self.coordinator.async_request_refresh()
@@ -41,7 +41,7 @@ rules:
# Gold
devices: done
diagnostics: done
diagnostics: todo
discovery: done
discovery-update-info:
status: exempt
+1 -62
View File
@@ -5,9 +5,8 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
from fumis import FumisInfo, StoveAlert, StoveError, StoveState, StoveStatus
from fumis import FumisInfo, StoveState, StoveStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -35,52 +34,15 @@ from .entity import FumisEntity
PARALLEL_UPDATES = 0
def _code_to_state(code: StoveAlert | StoveError | None) -> str | None:
"""Convert a stove alert or error code to a sensor state value.
Returns "none" when there is no active alert/error, None when the code
is unknown, or the enum member name in lowercase for known codes.
"""
if code is None:
return "none"
if code.name == "UNKNOWN":
return None
return code.name.lower()
def _code_to_attr(code: StoveAlert | StoveError | None) -> dict[str, str | None]:
"""Convert a stove alert or error code to extra state attributes."""
if code is None or code.name == "UNKNOWN":
return {"code": None}
return {"code": code.value}
@dataclass(frozen=True, kw_only=True)
class FumisSensorEntityDescription(SensorEntityDescription):
"""Describes a Fumis sensor entity."""
attr_fn: Callable[[FumisInfo], dict[str, Any]] | None = None
has_fn: Callable[[FumisInfo], bool] = lambda _: True
value_fn: Callable[[FumisInfo], datetime | float | int | str | None]
SENSORS: tuple[FumisSensorEntityDescription, ...] = (
FumisSensorEntityDescription(
key="alert",
translation_key="alert",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
"none",
*(
alert.name.lower()
for alert in StoveAlert
if alert != StoveAlert.UNKNOWN
),
],
value_fn=lambda data: _code_to_state(data.controller.stove_alert),
attr_fn=lambda data: _code_to_attr(data.controller.stove_alert),
),
FumisSensorEntityDescription(
key="combustion_chamber_temperature",
translation_key="combustion_chamber_temperature",
@@ -107,22 +69,6 @@ SENSORS: tuple[FumisSensorEntityDescription, ...] = (
else data.controller.stove_status.name.lower()
),
),
FumisSensorEntityDescription(
key="error",
translation_key="error",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
"none",
*(
error.name.lower()
for error in StoveError
if error != StoveError.UNKNOWN
),
],
value_fn=lambda data: _code_to_state(data.controller.stove_error),
attr_fn=lambda data: _code_to_attr(data.controller.stove_error),
),
FumisSensorEntityDescription(
key="fan_1_speed",
translation_key="fan_1_speed",
@@ -321,13 +267,6 @@ class FumisSensorEntity(FumisEntity, SensorEntity):
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return additional state attributes."""
if self.entity_description.attr_fn is None:
return None
return self.entity_description.attr_fn(self.coordinator.data)
@property
def native_value(self) -> datetime | float | int | str | None:
"""Return the sensor value."""
@@ -58,28 +58,7 @@
"name": "Sync clock"
}
},
"number": {
"fan_speed": {
"name": "Fan speed"
},
"power_level": {
"name": "Power level"
}
},
"sensor": {
"alert": {
"name": "Alert",
"state": {
"airflow_malfunction": "Airflow sensor malfunction",
"door_open": "Door open",
"flue_gas_warning": "Flue gas temperature warning",
"low_battery": "Low battery",
"low_fuel": "Low fuel level",
"none": "No alert",
"service_due": "Service due",
"speed_sensor_failure": "Speed sensor failure"
}
},
"combustion_chamber_temperature": {
"name": "Combustion chamber"
},
@@ -102,36 +81,6 @@
"wood_start": "Wood start"
}
},
"error": {
"name": "Error",
"state": {
"chimney_alarm": "Chimney alarm",
"chimney_dirty": "Chimney or burning pot dirty",
"door_alarm": "Door alarm",
"fire_error": "Fire error",
"flue_gas_overtemp": "Flue gas overtemperature",
"fuel_ignition_timeout": "Fuel ignition timeout",
"gas_alarm": "Gas alarm",
"general_error": "General error",
"grate_error": "Grate error",
"ignition_failed": "Ignition failed",
"mfdoor_alarm": "MFDoor alarm",
"no_pellet_alarm": "No pellet alarm",
"none": "No error",
"ntc1_alarm": "NTC1 alarm",
"ntc2_alarm": "NTC2 alarm",
"ntc3_alarm": "NTC3 alarm",
"pressure_alarm": "Pressure alarm",
"pressure_sensor_off": "Pressure sensor off",
"safety_switch": "Safety switch tripped",
"sensor_t01_t02": "Sensor T01/T02 malfunction",
"sensor_t01_t03": "Sensor T01/T03 malfunction",
"sensor_t02": "Sensor T02 malfunction",
"sensor_t03_t05": "Sensor T03/T05 malfunction",
"sensor_t04": "Sensor T04 malfunction",
"tc1_alarm": "TC1 alarm"
}
},
"fan_1_speed": {
"name": "Fan 1 speed"
},
@@ -3,8 +3,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -31,6 +31,21 @@
"name": "Garage door is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Garage door",
"triggers": {
"closed": {
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for:
required: true
default: 00:00:00
@@ -87,6 +87,17 @@ DESCRIPTIONS = (
char=Valve.remaining_open_time,
device_class=NumberDeviceClass.DURATION,
),
GardenaBluetoothNumberEntityDescription(
key=AquaContourWatering.remaining_watering_time.unique_id,
translation_key="remaining_watering_time",
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=0.0,
native_max_value=24 * 60 * 60,
native_step=60.0,
entity_category=EntityCategory.DIAGNOSTIC,
char=AquaContourWatering.remaining_watering_time,
device_class=NumberDeviceClass.DURATION,
),
GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.rain_pause.unique_id,
translation_key="rain_pause",
@@ -8,7 +8,6 @@ from datetime import UTC, datetime, timedelta
from gardena_bluetooth.const import (
AquaContourBattery,
AquaContourWatering,
Battery,
EventHistory,
FlowStatistics,
@@ -219,22 +218,7 @@ async def async_setup_entry(
if description.char.unique_id in coordinator.characteristics
]
if Valve.remaining_open_time.unique_id in coordinator.characteristics:
entities.append(
GardenaBluetoothRemainSensor(
coordinator, Valve.remaining_open_time, "remaining_open_timestamp"
)
)
if (
AquaContourWatering.remaining_watering_time.unique_id
in coordinator.characteristics
):
entities.append(
GardenaBluetoothRemainSensor(
coordinator,
AquaContourWatering.remaining_watering_time,
"remaining_watering_timestamp",
)
)
entities.append(GardenaBluetoothRemainSensor(coordinator))
async_add_entities(entities)
@@ -261,21 +245,18 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_native_value: datetime | None = None
_attr_translation_key = "remaining_open_timestamp"
def __init__(
self,
coordinator: GardenaBluetoothCoordinator,
char: Characteristic[int],
key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, {char.uuid})
self._attr_unique_id = f"{coordinator.address}-{key}"
self._attr_translation_key = key
self._char = char
super().__init__(coordinator, {Valve.remaining_open_time.uuid})
self._attr_unique_id = f"{coordinator.address}-remaining_open_timestamp"
def _handle_coordinator_update(self) -> None:
value = self.coordinator.get_cached(self._char)
value = self.coordinator.get_cached(Valve.remaining_open_time)
if not value:
self._attr_native_value = None
super()._handle_coordinator_update()
@@ -290,7 +271,8 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
error = time - self._attr_native_value
if abs(error.total_seconds()) > 10:
self._attr_native_value = time
super()._handle_coordinator_update()
super()._handle_coordinator_update()
return
@property
def available(self) -> bool:
@@ -134,9 +134,6 @@
"remaining_open_timestamp": {
"name": "Valve closing"
},
"remaining_watering_timestamp": {
"name": "Watering finished"
},
"sensor_battery_level": {
"name": "Sensor battery"
},
@@ -3,8 +3,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -31,6 +31,21 @@
"name": "Gate is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Gate",
"triggers": {
"closed": {
+6 -2
View File
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for:
required: true
default: 00:00:00
+1 -9
View File
@@ -9,10 +9,8 @@ from typing import cast
from aiohttp import web
import voluptuous as vol
from homeassistant.auth.permissions import filter_entity_ids_by_permission
from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.components import frontend
from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.components.recorder import get_instance, history
from homeassistant.components.recorder.util import session_scope
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
@@ -85,12 +83,6 @@ class HistoryPeriodView(HomeAssistantView):
"Invalid filter_entity_id", HTTPStatus.BAD_REQUEST
)
entity_ids = filter_entity_ids_by_permission(
request[KEY_HASS_USER], entity_ids, POLICY_READ
)
if not entity_ids:
return self.json([])
now = dt_util.utcnow()
if datetime_:
start_time = dt_util.as_utc(datetime_)
@@ -11,8 +11,6 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.auth.permissions import filter_entity_ids_by_permission
from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.components import websocket_api
from homeassistant.components.recorder import get_instance, history
from homeassistant.components.websocket_api import ActiveConnection, messages
@@ -140,13 +138,6 @@ async def ws_get_history_during_period(
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
return
entity_ids = filter_entity_ids_by_permission(
connection.user, entity_ids, POLICY_READ
)
if not entity_ids:
connection.send_result(msg["id"], {})
return
include_start_time_state = msg["include_start_time_state"]
no_attributes = msg["no_attributes"]
@@ -453,13 +444,6 @@ async def ws_stream(
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
return
entity_ids = filter_entity_ids_by_permission(
connection.user, entity_ids, POLICY_READ
)
if not entity_ids:
_async_send_empty_response(connection, msg_id, start_time, end_time)
return
include_start_time_state = msg["include_start_time_state"]
significant_changes_only = msg["significant_changes_only"]
no_attributes = msg["no_attributes"]
@@ -637,19 +637,16 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
options.update(await self.get_options_definitions(resolved_program_key))
for option in options.values():
option_event_key = EventKey(option.key)
if (
option_event_key not in events
and option.constraints
and (option_default_value := option.constraints.default) is not None
):
option_value = option.constraints.default if option.constraints else None
if option_value is not None:
option_event_key = EventKey(option.key)
events[option_event_key] = Event(
option_event_key,
option.key.value,
0,
"",
"",
option_default_value,
option_value,
option.name,
unit=option.unit,
)
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["huawei_lte_api.Session"],
"requirements": ["huawei-lte-api==1.11.0", "url-normalize==3.0.0"],
"requirements": ["huawei-lte-api==1.11.0", "url-normalize==2.2.1"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
@@ -7,8 +7,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
.condition_common_for: &condition_common_for
target: *condition_humidifier_target
@@ -162,6 +162,21 @@
"message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}."
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"set_humidity": {
"description": "Sets the target humidity of a humidifier.",
@@ -7,8 +7,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
@@ -25,8 +25,11 @@ is_value:
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
threshold:
required: true
selector:
@@ -20,6 +20,21 @@
"name": "Relative humidity"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Humidity",
"triggers": {
"changed": {
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
@@ -8,8 +8,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -45,6 +45,21 @@
"name": "Illuminance"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Illuminance",
"triggers": {
"changed": {
@@ -3,8 +3,12 @@
required: true
default: any
selector:
automation_behavior:
mode: trigger
select:
translation_key: trigger_behavior
options:
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
@@ -12,7 +12,6 @@ from .coordinator import IndevoltConfigEntry, IndevoltCoordinator
from .services import async_setup_services
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
@@ -1,154 +0,0 @@
"""Binary sensor platform for Indevolt integration."""
from dataclasses import dataclass
from typing import Final
from indevolt_api import IndevoltBattery, IndevoltGrid, IndevoltSystem
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 IndevoltConfigEntry
from .coordinator import IndevoltCoordinator
from .entity import IndevoltEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Custom entity description class for Indevolt binary sensors."""
on_value: int = 1
off_value: int = 0
generation: tuple[int, ...] = (1, 2)
BINARY_SENSORS: Final = (
# Electricity Meter Status
IndevoltBinarySensorEntityDescription(
key=IndevoltGrid.METER_CONNECTED,
translation_key="meter_connected",
on_value=1000,
off_value=1001,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
# Electric Heating States
IndevoltBinarySensorEntityDescription(
key=IndevoltSystem.HEATING_STATE,
generation=(1,),
translation_key="electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
IndevoltBinarySensorEntityDescription(
key=IndevoltBattery.MAIN_HEATING_STATE,
generation=(2,),
translation_key="main_electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
IndevoltBinarySensorEntityDescription(
key=IndevoltBattery.PACK_1_HEATING_STATE,
generation=(2,),
translation_key="battery_pack_1_electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
IndevoltBinarySensorEntityDescription(
key=IndevoltBattery.PACK_2_HEATING_STATE,
generation=(2,),
translation_key="battery_pack_2_electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
IndevoltBinarySensorEntityDescription(
key=IndevoltBattery.PACK_3_HEATING_STATE,
generation=(2,),
translation_key="battery_pack_3_electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
IndevoltBinarySensorEntityDescription(
key=IndevoltBattery.PACK_4_HEATING_STATE,
generation=(2,),
translation_key="battery_pack_4_electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
IndevoltBinarySensorEntityDescription(
key=IndevoltBattery.PACK_5_HEATING_STATE,
generation=(2,),
translation_key="battery_pack_5_electric_heating_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
)
# Sensor per battery pack: (serial_number_key, heating_state_key)
BATTERY_PACK_SENSOR_KEYS = [
(IndevoltBattery.PACK_1_SERIAL_NUMBER, IndevoltBattery.PACK_1_HEATING_STATE),
(IndevoltBattery.PACK_2_SERIAL_NUMBER, IndevoltBattery.PACK_2_HEATING_STATE),
(IndevoltBattery.PACK_3_SERIAL_NUMBER, IndevoltBattery.PACK_3_HEATING_STATE),
(IndevoltBattery.PACK_4_SERIAL_NUMBER, IndevoltBattery.PACK_4_HEATING_STATE),
(IndevoltBattery.PACK_5_SERIAL_NUMBER, IndevoltBattery.PACK_5_HEATING_STATE),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: IndevoltConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the binary sensor platform for Indevolt."""
coordinator = entry.runtime_data
device_gen = coordinator.generation
excluded_keys: set[str] = set()
for sn_key, heating_key in BATTERY_PACK_SENSOR_KEYS:
if not coordinator.data.get(sn_key):
excluded_keys.add(heating_key)
async_add_entities(
IndevoltBinarySensorEntity(coordinator, description)
for description in BINARY_SENSORS
if device_gen in description.generation and description.key not in excluded_keys
)
class IndevoltBinarySensorEntity(IndevoltEntity, BinarySensorEntity):
"""Represents a binary sensor entity for Indevolt devices."""
entity_description: IndevoltBinarySensorEntityDescription
def __init__(
self,
coordinator: IndevoltCoordinator,
description: IndevoltBinarySensorEntityDescription,
) -> None:
"""Initialize the Indevolt binary sensor entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{self.serial_number}_{description.key}"
@property
def is_on(self) -> bool | None:
"""Return on/active state of the binary sensor."""
raw_value = self.coordinator.data.get(self.entity_description.key)
if raw_value == self.entity_description.on_value:
return True
if raw_value == self.entity_description.off_value:
return False
return None
@@ -38,9 +38,7 @@ SENSOR_KEYS: Final[dict[int, list[str]]] = {
IndevoltSolar.DC_INPUT_POWER_4,
IndevoltConfig.READ_DISCHARGE_LIMIT,
IndevoltGrid.METER_POWER_GEN1,
IndevoltGrid.METER_CONNECTED,
IndevoltSolar.CUMULATIVE_PRODUCTION,
IndevoltSystem.HEATING_STATE,
],
2: [
IndevoltSystem.OPERATING_MODE,
@@ -114,13 +112,6 @@ SENSOR_KEYS: Final[dict[int, list[str]]] = {
IndevoltConfig.READ_INVERTER_INPUT_LIMIT,
IndevoltConfig.READ_FEEDIN_POWER_LIMIT,
IndevoltConfig.READ_DISCHARGE_LIMIT,
IndevoltBattery.MAIN_HEATING_STATE,
IndevoltBattery.PACK_1_HEATING_STATE,
IndevoltBattery.PACK_2_HEATING_STATE,
IndevoltBattery.PACK_3_HEATING_STATE,
IndevoltBattery.PACK_4_HEATING_STATE,
IndevoltBattery.PACK_5_HEATING_STATE,
IndevoltGrid.METER_CONNECTED,
IndevoltSolar.CUMULATIVE_PRODUCTION,
],
}
@@ -35,32 +35,6 @@
}
},
"entity": {
"binary_sensor": {
"battery_pack_1_electric_heating_state": {
"name": "Battery pack 1 electric heating"
},
"battery_pack_2_electric_heating_state": {
"name": "Battery pack 2 electric heating"
},
"battery_pack_3_electric_heating_state": {
"name": "Battery pack 3 electric heating"
},
"battery_pack_4_electric_heating_state": {
"name": "Battery pack 4 electric heating"
},
"battery_pack_5_electric_heating_state": {
"name": "Battery pack 5 electric heating"
},
"electric_heating_state": {
"name": "Electric heating"
},
"main_electric_heating_state": {
"name": "Main electric heating"
},
"meter_connected": {
"name": "Meter connected"
}
},
"button": {
"stop": {
"name": "Enable standby mode"
@@ -7,12 +7,7 @@ from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from jvcprojector import (
JvcProjector,
JvcProjectorCommandError,
JvcProjectorTimeoutError,
command as cmd,
)
from jvcprojector import JvcProjector, JvcProjectorTimeoutError, command as cmd
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -149,16 +144,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
self, command: type[Command], new_state: dict[type[Command], str]
) -> str | None:
"""Update state with the current value of a command."""
try:
value = await self.device.get(command)
except JvcProjectorCommandError as err:
_LOGGER.warning("Command %s failed: %s", command.name, err)
cached = self.state.get(command)
if command is cmd.Power and cached is None:
raise UpdateFailed(
f"Failed to fetch {command.name} and no cached value is available"
) from err
return cached
value = await self.device.get(command)
if value != self.state.get(command):
new_state[command] = value
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==2.0.6"]
"requirements": ["pyjvcprojector==2.0.5"]
}
@@ -7,8 +7,11 @@
required: true
default: any
selector:
automation_behavior:
mode: condition
select:
translation_key: condition_behavior
options:
- all
- any
for:
required: true
default: 00:00:00
@@ -79,6 +79,21 @@
}
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"dock": {
"description": "Returns a lawn mower to its dock.",

Some files were not shown because too many files have changed in this diff Show More