mirror of
https://github.com/home-assistant/core.git
synced 2026-05-07 10:26:51 +02:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef7eed579b | |||
| 568a0085fe | |||
| f5363db26f | |||
| 3be1aa5441 | |||
| 7dbffb7375 | |||
| 07c4025d47 | |||
| 3e3e425aa5 | |||
| 162a4fc385 | |||
| ef6fd92079 | |||
| 4ad71a070a | |||
| f33ad12f5e | |||
| da7fbb0dd6 | |||
| 81137345a3 | |||
| d3e77d4195 | |||
| ce977e90a5 | |||
| 2871b87344 | |||
| d82ce1e22d | |||
| b8bb2e0090 | |||
| 1b81cfe3ca | |||
| 0a3f0d90c3 | |||
| 84d566a02c | |||
| 0e0d54e4b6 | |||
| 5b05061def | |||
| e0bf76769a | |||
| 63868bc169 | |||
| b8b7169371 | |||
| 1cc778954f | |||
| 3ba3ecdef3 | |||
| 5c57fc6e14 | |||
| 2da440043a | |||
| 4f34725e53 | |||
| d03bec2f44 | |||
| 57c37fc10c | |||
| fd98594143 | |||
| 894547abed | |||
| b48060674c | |||
| 6f2aa7852a | |||
| 9d53645468 | |||
| a3f1c067f7 | |||
| cef97973d0 | |||
| 7bb297a3fc | |||
| 7e2b8e1a48 | |||
| 013c5e7f7c | |||
| 7cb1d5b8ab | |||
| 57d9e8ea6f | |||
| 32743fcf8d | |||
| f4637db26d | |||
| b4bfe6b80b | |||
| 278f25ec6e | |||
| 39d3bc3e53 | |||
| bb41a2df9f | |||
| 284242b90e | |||
| a95c216983 | |||
| d41a3ae0cd | |||
| 0dfbe3ef84 | |||
| 71fc725d75 | |||
| d41c9aee52 | |||
| 8091f511b8 | |||
| a7baedc22b | |||
| 05bfb3a52e | |||
| 2a5b95ba4d | |||
| 3dd972cc7a | |||
| acd9dd218a | |||
| 6552cf8f7a | |||
| e4e4785225 | |||
| d531ce8d1d | |||
| 0224928655 |
@@ -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
|
||||
|
||||
@@ -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.]+)\""],
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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,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
@@ -1 +1 @@
|
||||
3.14.3
|
||||
3.14.2
|
||||
|
||||
Generated
+2
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["fumis"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["fumis==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,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
Reference in New Issue
Block a user