Compare commits

..

67 Commits

Author SHA1 Message Date
Franck Nijhof ef7eed579b Bump version to 2026.5.0b0 2026-04-29 16:40:46 +00:00
Franck Nijhof 568a0085fe Bump version to 2026.5.0 2026-04-29 15:50:10 +00:00
Joakim Plate f5363db26f Move finish watering to sensor (#169476)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 17:34:38 +02:00
Erik Montnemery 3be1aa5441 Include errors in script trace when continue_on_error is set (#168676) 2026-04-29 17:30:47 +02:00
Paul Bottein 7dbffb7375 Update frontend to 20260429.0 (#169475) 2026-04-29 17:21:40 +02:00
A. Gideonse 07c4025d47 Add indevolt binary sensor platform (#169375)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 16:19:17 +01:00
Franck Nijhof 3e3e425aa5 Bump Fumis integration to platinum quality scale (#169443) 2026-04-29 17:14:50 +02:00
Erik Montnemery 162a4fc385 Use automation behavior selector in triggers and conditions (#169438) 2026-04-29 17:10:50 +02:00
Manu ef6fd92079 Add notify entities to Mobile app integration (#168510)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-29 17:06:13 +02:00
Erik Montnemery 4ad71a070a Improve timer icons (#169474) 2026-04-29 17:02:22 +02:00
Erik Montnemery f33ad12f5e Correct entity_id change for automations (#169470) 2026-04-29 16:30:02 +02:00
Erik Montnemery da7fbb0dd6 Correct entity_id change for scripts (#169472) 2026-04-29 16:29:25 +02:00
Abílio Costa 81137345a3 Extract triggers/conditions/services for non-primary entities (#169441) 2026-04-29 15:28:09 +01:00
Erik Montnemery d3e77d4195 Add timer triggers (#169450) 2026-04-29 16:27:52 +02:00
renovate[bot] ce977e90a5 Update cryptography to 47.0.0 (#169465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 16:22:16 +02:00
Martin Hjelmare 2871b87344 Revert "Include indirect automation references in device view (#167719)" (#169471) 2026-04-29 15:15:50 +01:00
renovate[bot] d82ce1e22d Update ruff (#169473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 16:11:22 +02:00
Guido Schmitz b8bb2e0090 Use uptime sensor class in devolo Home Network (#169469) 2026-04-29 16:10:39 +02:00
Erik Montnemery 1b81cfe3ca Make it always optional to specify trigger and condition options (#169467) 2026-04-29 15:06:05 +01:00
renovate[bot] 0a3f0d90c3 Update url-normalize to 3.0.0 (#169466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:48:05 +02:00
renovate[bot] 84d566a02c Update pyOpenSSL to 26.1.0 (#169464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:47:52 +02:00
renovate[bot] 0e0d54e4b6 Update uv to 0.11.8 (#169463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:47:39 +02:00
epenet 5b05061def Fix plex sensor test broken by Python 3.14.3 asyncio changes (#169448) 2026-04-29 15:22:34 +02:00
renovate[bot] e0bf76769a Update ruff (#169461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:17:04 +02:00
renovate[bot] 63868bc169 Migrate Renovate config (#169462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:16:26 +02:00
Bram Kragten b8b7169371 Add automation behavior selector (#166484)
Co-authored-by: Erik <erik@montnemery.com>
2026-04-29 15:10:47 +02:00
Maciej Bieniek 1cc778954f Use new UPTIME sensor class in Brother (#169457) 2026-04-29 14:54:55 +02:00
Maciej Bieniek 3ba3ecdef3 Use new UPTIME sensor class in NAM (#169458) 2026-04-29 14:54:14 +02:00
Ronald van der Meer 5c57fc6e14 Fix Duco HTTPS polling performance by lowering SCAN_INTERVAL to 10 seconds (#169453) 2026-04-29 14:25:04 +02:00
epenet 2da440043a Fix Sonos group regroup race when entity is not yet registered (#169445) 2026-04-29 14:13:59 +02:00
epenet 4f34725e53 Fix flaky portainer test_device_registry (#169456)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:11:07 +02:00
epenet d03bec2f44 Fix race in devolo Home Network device tracker device lookup (#169454)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:08:00 +02:00
epenet 57c37fc10c Fix race in Ping device tracker device lookup (#169432)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:02:37 +02:00
Simone Chemelli fd98594143 Use defaults for device class UPTIME in Fritz (#169149) 2026-04-29 12:34:25 +01:00
Robert Svensson 894547abed Add Axis doorbell event platform (#169422) 2026-04-29 12:29:58 +01:00
Luis Miranda b48060674c Add OMIE integration (#150399)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-29 12:10:22 +01:00
vturekhanov 6f2aa7852a Fix availability state for bridged Matter devices (#165078)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-29 13:05:15 +02:00
Abílio Costa 9d53645468 Remove LLM test instruction (#169442) 2026-04-29 12:58:47 +02:00
Simone Chemelli a3f1c067f7 Fix host connections for Fritz (#169434) 2026-04-29 12:57:05 +02:00
Tomer cef97973d0 Victron GX device_tracker optional attributes (#168646)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:56:13 +02:00
TheJulianJES 7bb297a3fc Bump ZHA to 1.3.0 (#169433) 2026-04-29 12:51:18 +02:00
Maciej Bieniek 7e2b8e1a48 Bump aioshelly to 13.24.2 (#169440) 2026-04-29 12:50:44 +02:00
Franck Nijhof 013c5e7f7c Add diagnostics to Fumis integration (#169437) 2026-04-29 12:38:09 +02:00
Abílio Costa 7cb1d5b8ab Allow targeting non-primary entities in conditions (#169291) 2026-04-29 12:25:26 +02:00
Paulus Schoutsen 57d9e8ea6f Filter history API responses by per-entity read permissions (#169236)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:16:49 +02:00
Andrew Ng 32743fcf8d Fix Acaia battery sensor going unavailable on first-session disconnect (#169420)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-04-29 12:12:44 +02:00
Simone Chemelli f4637db26d Add routine management to Alexa Devices (#166291) 2026-04-29 11:45:03 +02:00
Erik Montnemery b4bfe6b80b Rename timer last_action to last_transition (#169430) 2026-04-29 11:35:36 +02:00
Andrej Walilko 278f25ec6e Redact sensitive api creds before logging message in websocket api (#169326)
Co-authored-by: Andrej Walilko <awalilko@liquidweb.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-29 11:15:05 +02:00
Robert Resch 39d3bc3e53 Bump deebot-client to 18.2.0 (#169003) 2026-04-29 11:13:14 +02:00
Yabing Yi bb41a2df9f Fix logbook spam by including image domain in ALWAYS_CONTINUOUS_DOMAINS (#169240)
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-29 10:58:13 +02:00
Petar Petrov 284242b90e Copy unit_of_measurement onto energy inverted power sensor (#169427) 2026-04-29 10:56:08 +02:00
Erik Montnemery a95c216983 Unload scripts created by websocket command execute_script (#169368)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-04-29 10:24:24 +02:00
Simone Chemelli d41a3ae0cd Use defaults for device class UPTIME in Shelly (#169148) 2026-04-29 10:12:18 +02:00
J. Nick Koston 0dfbe3ef84 Expose async_clear_advertisement_history in the bluetooth API (#169191) 2026-04-29 10:11:27 +02:00
Franck Nijhof 71fc725d75 Extract state template functions into a state Jinja2 extension (#169034) 2026-04-29 10:03:38 +02:00
Shay Levy d41c9aee52 Bump aioshelly to 13.24.1 (#169426)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-04-29 10:53:38 +03:00
epenet 8091f511b8 Reject manifest dependencies on core integrations in hassfest (#169425) 2026-04-29 09:52:46 +02:00
Franck Nijhof a7baedc22b Add error and alert sensors to Fumis integration (#169307) 2026-04-29 09:51:22 +02:00
Franck Nijhof 05bfb3a52e Add number platform to Fumis integration (#169100) 2026-04-29 09:39:15 +02:00
Robert Resch 2a5b95ba4d Require hass in Template (#169292)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 09:26:32 +02:00
Steve Easley 3dd972cc7a Fix jvcprojector entities going unavailable on transient command errors (#168985) 2026-04-29 09:21:53 +02:00
Marc Mueller acd9dd218a Protect CI cache save against cancellation (#168310) 2026-04-29 09:20:37 +02:00
J. Diego Rodríguez Royo 6552cf8f7a Keep options values when chaging or starting program on Home Connect (#168575) 2026-04-29 09:19:41 +02:00
Artur Pragacz e4e4785225 Clean up entity_service_call tests (#169170)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 09:17:45 +02:00
G Johansson d531ce8d1d Use async_on_create_entry in bayesian (#169218)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 09:14:58 +02:00
Stefan Agner 0224928655 Bump python-otbr-api to 2.10.0 (#169370)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-04-29 09:10:26 +02:00
298 changed files with 11328 additions and 4288 deletions
@@ -15,7 +15,6 @@ 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,7 +18,6 @@ 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
+3 -2
View File
@@ -6,7 +6,7 @@
"pep621",
"pip_requirements",
"pre-commit",
"regex",
"custom.regex",
"homeassistant-manifest"
],
@@ -27,8 +27,9 @@
]
},
"regexManagers": [
"customManagers": [
{
"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.02.0"
BASE_IMAGE_VERSION: "2026.01.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
+29 -3
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
key: >-
@@ -374,7 +374,8 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-uv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -398,6 +399,7 @@ 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 }}
@@ -431,7 +433,10 @@ jobs:
sudo chmod -R 755 ${APT_CACHE_BASE}
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
if: |
always()
&& steps.cache-apt-check.outputs.cache-hit != 'true'
&& steps.install-os-deps.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
@@ -441,6 +446,7 @@ 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
@@ -471,6 +477,26 @@ 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.10
rev: v0.15.12
hooks:
- id: ruff-check
args:
+1 -1
View File
@@ -1 +1 @@
3.14.3
3.14.2
Generated
+2
View File
@@ -1241,6 +1241,8 @@ 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
+16 -1
View File
@@ -2,7 +2,8 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING
import voluptuous as vol
@@ -13,6 +14,9 @@ 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__ = [
@@ -22,10 +26,21 @@ __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._restored_data is not None
return super().available or self.native_value is not None
@@ -4,11 +4,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
# --- Unit lists for multi-unit pollutants ---
@@ -237,21 +237,6 @@
"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,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -7,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
.condition_common_for: &condition_common_for
target: *condition_common_target
@@ -160,21 +160,6 @@
"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,12 +7,8 @@
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
@@ -11,6 +11,7 @@ from .services import async_setup_services
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
@@ -0,0 +1,55 @@
"""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,12 +12,13 @@ from aioamazondevices.structures import AmazonDevice
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
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
@@ -64,6 +65,13 @@ 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."""
@@ -92,8 +100,13 @@ 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(
@@ -116,3 +129,23 @@ 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,9 +2,10 @@
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType, 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
@@ -50,3 +51,32 @@ 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,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -72,19 +72,6 @@
"id": "Answer ID",
"sentences": "Sentences"
}
},
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
@@ -7,12 +7,8 @@
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
@@ -194,6 +194,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"switch",
"temperature",
"text",
"timer",
"todo",
"update",
"vacuum",
@@ -901,6 +902,10 @@ 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()
+7 -1
View File
@@ -18,4 +18,10 @@ DEFAULT_STREAM_PROFILE = "No stream profile"
DEFAULT_TRIGGER_TIME = 0
DEFAULT_VIDEO_SOURCE = "No video source"
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.LIGHT, Platform.SWITCH]
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.SWITCH,
]
+62
View File
@@ -0,0 +1,62 @@
"""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()
+19 -5
View File
@@ -30,19 +30,33 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
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,
),
}
@@ -3,16 +3,14 @@
entity:
- domain: binary_sensor
device_class: battery
primary_entities_only: false
fields:
behavior: &condition_behavior
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for: &condition_for
required: true
default: 00:00:00
@@ -42,6 +40,7 @@ is_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
@@ -51,6 +50,7 @@ is_not_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
@@ -60,6 +60,7 @@ is_level:
entity:
- domain: sensor
device_class: battery
primary_entities_only: false
fields:
behavior: *condition_behavior
threshold:
@@ -69,21 +69,6 @@
"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,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -33,11 +33,13 @@ 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 (
@@ -62,7 +64,6 @@ 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,
@@ -373,26 +374,6 @@ 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]:
@@ -420,48 +401,12 @@ 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(),
)
}
@@ -497,27 +442,17 @@ class BayesianConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
name: str = options[CONF_NAME]
return name
@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)
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
class ObservationSubentryFlowHandler(ConfigSubentryFlow):
@@ -58,6 +58,7 @@ 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,
@@ -116,6 +117,7 @@ __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,6 +207,19 @@ 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,
+1 -2
View File
@@ -293,9 +293,8 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="uptime",
translation_key="last_restart",
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda data: data.uptime,
),
@@ -151,9 +151,6 @@
"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,11 +7,8 @@ is_event_active:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -64,12 +64,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_offset_type": {
"options": {
"after": "After",
@@ -7,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
@@ -271,21 +271,6 @@
"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,12 +7,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -7,11 +7,8 @@ is_value:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
threshold:
required: true
selector:
@@ -43,21 +43,6 @@
}
}
},
"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,12 +7,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
@@ -3,11 +3,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -210,21 +210,6 @@
"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.",
+2 -6
View File
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
@@ -7,6 +7,7 @@ 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 (
@@ -17,6 +18,7 @@ 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 (
@@ -123,6 +125,25 @@ 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.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
value_func=_last_restart,
),
}
@@ -75,9 +75,6 @@
"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,11 +3,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -31,21 +31,6 @@
"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": {
+2 -6
View File
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
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=30)
SCAN_INTERVAL = timedelta(seconds=10)
@@ -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.1.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==18.2.0"]
}
+6 -5
View File
@@ -715,6 +715,9 @@ 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:
@@ -763,13 +766,11 @@ class EnergyPowerSensor(SensorEntity):
# Check first sensor
if source_entry := entity_reg.async_get(self._source_sensors[0]):
device_id = source_entry.device_id
# For combined mode, always use Watts because we may have different source units; for inverted mode, copy source unit
# 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.
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)
+2 -5
View File
@@ -7,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
-13
View File
@@ -93,24 +93,11 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"direction": {
"options": {
"forward": "Forward",
"reverse": "Reverse"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
+2 -6
View File
@@ -7,12 +7,8 @@
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
automation_behavior:
mode: trigger
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 = await self._async_update_hosts_info()
device_hosts.update(await self._async_update_hosts_info())
entity_reg: er.EntityRegistry = er.async_get(self.hass)
config_entry = self.config_entry
-1
View File
@@ -294,7 +294,6 @@ 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,9 +120,6 @@
"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==20260325.8"]
"requirements": ["home-assistant-frontend==20260429.0"]
}
@@ -11,6 +11,7 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
]
@@ -0,0 +1,21 @@
"""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,13 +5,64 @@
"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": "bronze",
"quality_scale": "platinum",
"requirements": ["fumis==0.4.0"]
}
+97
View File
@@ -0,0 +1,97 @@
"""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: todo
diagnostics: done
discovery: done
discovery-update-info:
status: exempt
+62 -1
View File
@@ -5,8 +5,9 @@ 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, StoveState, StoveStatus
from fumis import FumisInfo, StoveAlert, StoveError, StoveState, StoveStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -34,15 +35,52 @@ 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",
@@ -69,6 +107,22 @@ 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",
@@ -267,6 +321,13 @@ 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,7 +58,28 @@
"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"
},
@@ -81,6 +102,36 @@
"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,11 +3,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -31,21 +31,6 @@
"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,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
@@ -87,17 +87,6 @@ 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,6 +8,7 @@ from datetime import UTC, datetime, timedelta
from gardena_bluetooth.const import (
AquaContourBattery,
AquaContourWatering,
Battery,
EventHistory,
FlowStatistics,
@@ -218,7 +219,22 @@ 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))
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",
)
)
async_add_entities(entities)
@@ -245,18 +261,21 @@ 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, {Valve.remaining_open_time.uuid})
self._attr_unique_id = f"{coordinator.address}-remaining_open_timestamp"
super().__init__(coordinator, {char.uuid})
self._attr_unique_id = f"{coordinator.address}-{key}"
self._attr_translation_key = key
self._char = char
def _handle_coordinator_update(self) -> None:
value = self.coordinator.get_cached(Valve.remaining_open_time)
value = self.coordinator.get_cached(self._char)
if not value:
self._attr_native_value = None
super()._handle_coordinator_update()
@@ -271,8 +290,7 @@ 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()
return
super()._handle_coordinator_update()
@property
def available(self) -> bool:
@@ -134,6 +134,9 @@
"remaining_open_timestamp": {
"name": "Valve closing"
},
"remaining_watering_timestamp": {
"name": "Watering finished"
},
"sensor_battery_level": {
"name": "Sensor battery"
},
@@ -3,11 +3,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -31,21 +31,6 @@
"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": {
+2 -6
View File
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
+9 -1
View File
@@ -9,8 +9,10 @@ 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, HomeAssistantView
from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, 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
@@ -83,6 +85,12 @@ 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,6 +11,8 @@ 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
@@ -138,6 +140,13 @@ 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"]
@@ -444,6 +453,13 @@ 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,16 +637,19 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
options.update(await self.get_options_definitions(resolved_program_key))
for option in options.values():
option_value = option.constraints.default if option.constraints else None
if option_value is not None:
option_event_key = EventKey(option.key)
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
):
events[option_event_key] = Event(
option_event_key,
option.key.value,
0,
"",
"",
option_value,
option_default_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==2.2.1"],
"requirements": ["huawei-lte-api==1.11.0", "url-normalize==3.0.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
@@ -7,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
.condition_common_for: &condition_common_for
target: *condition_humidifier_target
@@ -162,21 +162,6 @@
"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,12 +7,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -25,11 +25,8 @@ is_value:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
threshold:
required: true
selector:
@@ -20,21 +20,6 @@
"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,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -8,11 +8,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -45,21 +45,6 @@
"name": "Illuminance"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Illuminance",
"triggers": {
"changed": {
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -12,6 +12,7 @@ from .coordinator import IndevoltConfigEntry, IndevoltCoordinator
from .services import async_setup_services
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
@@ -0,0 +1,154 @@
"""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,7 +38,9 @@ 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,
@@ -112,6 +114,13 @@ 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,6 +35,32 @@
}
},
"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,7 +7,12 @@ from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from jvcprojector import JvcProjector, JvcProjectorTimeoutError, command as cmd
from jvcprojector import (
JvcProjector,
JvcProjectorCommandError,
JvcProjectorTimeoutError,
command as cmd,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -144,7 +149,16 @@ 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."""
value = await self.device.get(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
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.5"]
"requirements": ["pyjvcprojector==2.0.6"]
}
@@ -7,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -79,21 +79,6 @@
}
}
},
"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