Compare commits

..

1 Commits

Author SHA1 Message Date
Erik 918185fda4 Extend script continue_on_error suppression when calling actions 2026-04-21 10:01:08 +02:00
603 changed files with 2339 additions and 23252 deletions
+4 -5
View File
@@ -27,13 +27,12 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- List specific comments for each file/line that needs attention
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -1,5 +1,5 @@
---
name: ha-integration-knowledge
name: Home Assistant Integration knowledge
description: Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
---
@@ -14,8 +14,6 @@ description: Everything you need to know to build, test and review Home Assistan
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
- 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
+1 -1
View File
@@ -38,4 +38,4 @@ When validation guarantees a dict key exists, prefer direct key access (`data["k
# Skills
- ha-integration-knowledge: .claude/skills/ha-integration-knowledge/SKILL.md
- Home Assistant Integration knowledge: .claude/skills/integrations/SKILL.md
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:python"
+1 -1
View File
@@ -18,7 +18,7 @@ repos:
exclude_types: [csv, json, html]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.24.1
rev: v1.24.0
hooks:
- id: zizmor
args:
Generated
-4
View File
@@ -400,8 +400,6 @@ CLAUDE.md @home-assistant/core
/tests/components/dnsip/ @gjohansson-ST
/homeassistant/components/door/ @home-assistant/core
/tests/components/door/ @home-assistant/core
/homeassistant/components/doorbell/ @home-assistant/core
/tests/components/doorbell/ @home-assistant/core
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery
@@ -1255,8 +1253,6 @@ CLAUDE.md @home-assistant/core
/tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek @ab3lson
/tests/components/open_router/ @joostlek @ab3lson
/homeassistant/components/openai_conversation/ @Shulyaka
/tests/components/openai_conversation/ @Shulyaka
/homeassistant/components/opendisplay/ @g4bri3lDev
/tests/components/opendisplay/ @g4bri3lDev
/homeassistant/components/openerz/ @misialq
+19 -26
View File
@@ -30,7 +30,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import CONF_POLLING, DOMAIN, LOGGER
from .const import CONF_POLLING, DOMAIN, DOMAIN_DATA, LOGGER
from .services import async_setup_services
ATTR_DEVICE_NAME = "device_name"
@@ -67,16 +67,13 @@ class AbodeSystem:
logout_listener: CALLBACK_TYPE | None = None
type AbodeConfigEntry = ConfigEntry[AbodeSystem]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Abode integration from a config entry."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
@@ -102,54 +99,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> boo
except (AbodeException, ConnectTimeout, HTTPError) as ex:
raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex
entry.runtime_data = AbodeSystem(abode, polling)
hass.data[DOMAIN_DATA] = AbodeSystem(abode, polling)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await setup_hass_events(hass, entry)
await hass.async_add_executor_job(setup_abode_events, hass, entry)
await setup_hass_events(hass)
await hass.async_add_executor_job(setup_abode_events, hass)
return True
def _shutdown_client(abode: Abode) -> None:
"""Shutdown client."""
abode.events.stop()
abode.logout()
async def async_unload_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await hass.async_add_executor_job(_shutdown_client, entry.runtime_data.abode)
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.stop)
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.logout)
if logout_listener := entry.runtime_data.logout_listener:
if logout_listener := hass.data[DOMAIN_DATA].logout_listener:
logout_listener()
hass.data.pop(DOMAIN_DATA)
return unload_ok
async def setup_hass_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
async def setup_hass_events(hass: HomeAssistant) -> None:
"""Home Assistant start and stop callbacks."""
def logout(event: Event) -> None:
"""Logout of Abode."""
if not entry.runtime_data.polling:
entry.runtime_data.abode.events.stop()
if not hass.data[DOMAIN_DATA].polling:
hass.data[DOMAIN_DATA].abode.events.stop()
entry.runtime_data.abode.logout()
hass.data[DOMAIN_DATA].abode.logout()
LOGGER.info("Logged out of Abode")
if not entry.runtime_data.polling:
await hass.async_add_executor_job(entry.runtime_data.abode.events.start)
if not hass.data[DOMAIN_DATA].polling:
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.start)
entry.runtime_data.logout_listener = hass.bus.async_listen_once(
hass.data[DOMAIN_DATA].logout_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, logout
)
def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
def setup_abode_events(hass: HomeAssistant) -> None:
"""Event callbacks."""
def event_callback(event: str, event_json: dict[str, str]) -> None:
@@ -186,6 +179,6 @@ def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
]
for event in events:
entry.runtime_data.abode.events.add_event_callback(
hass.data[DOMAIN_DATA].abode.events.add_event_callback(
event, partial(event_callback, event)
)
@@ -9,20 +9,21 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry
from .const import DOMAIN_DATA
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode alarm control panel device."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
async_add_entities(
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
)
@@ -10,21 +10,22 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.enum import try_parse_enum
from . import AbodeConfigEntry
from .const import DOMAIN_DATA
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode binary sensor devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
device_types = [
"connectivity",
+5 -4
View File
@@ -12,13 +12,14 @@ import requests
from requests.models import Response
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import Throttle
from . import AbodeConfigEntry, AbodeSystem
from .const import LOGGER
from . import AbodeSystem
from .const import DOMAIN_DATA, LOGGER
from .entity import AbodeDevice
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
@@ -26,11 +27,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode camera devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
async_add_entities(
AbodeCamera(data, device, timeline.CAPTURE_IMAGE)
+7
View File
@@ -3,10 +3,17 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import AbodeSystem
LOGGER = logging.getLogger(__package__)
DOMAIN = "abode"
DOMAIN_DATA: HassKey[AbodeSystem] = HassKey(DOMAIN)
ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = "polling"
+4 -3
View File
@@ -5,20 +5,21 @@ from typing import Any
from jaraco.abode.devices.cover import Cover
from homeassistant.components.cover import CoverEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry
from .const import DOMAIN_DATA
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode cover devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
async_add_entities(
AbodeCover(data, device)
+2 -2
View File
@@ -7,7 +7,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from . import AbodeSystem
from .const import ATTRIBUTION, DOMAIN
from .const import ATTRIBUTION, DOMAIN, DOMAIN_DATA
class AbodeEntity(Entity):
@@ -29,7 +29,7 @@ class AbodeEntity(Entity):
self._update_connection_status,
)
self._data.entity_ids.add(self.entity_id)
self.hass.data[DOMAIN_DATA].entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from Abode connection status updates."""
+4 -3
View File
@@ -16,20 +16,21 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry
from .const import DOMAIN_DATA
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode light devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
async_add_entities(
AbodeLight(data, device)
+4 -3
View File
@@ -5,20 +5,21 @@ from typing import Any
from jaraco.abode.devices.lock import Lock
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry
from .const import DOMAIN_DATA
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode lock devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
async_add_entities(
AbodeLock(data, device)
+5 -3
View File
@@ -14,11 +14,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry, AbodeSystem
from . import AbodeSystem
from .const import DOMAIN_DATA
from .entity import AbodeDevice
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
@@ -64,11 +66,11 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode sensor devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
async_add_entities(
AbodeSensor(data, device, description)
+4 -18
View File
@@ -2,21 +2,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from jaraco.abode.exceptions import Exception as AbodeException
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import DOMAIN, LOGGER
if TYPE_CHECKING:
from . import AbodeConfigEntry, AbodeSystem
from .const import DOMAIN, DOMAIN_DATA, LOGGER
ATTR_SETTING = "setting"
ATTR_VALUE = "value"
@@ -31,21 +25,13 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
def _get_abode_system(hass: HomeAssistant) -> AbodeSystem:
"""Return the Abode system for the loaded config entry."""
entries: list[AbodeConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError("Abode integration is not loaded")
return entries[0].runtime_data
def _change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting."""
setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE]
try:
_get_abode_system(call.hass).abode.set_setting(setting, value)
call.hass.data[DOMAIN_DATA].abode.set_setting(setting, value)
except AbodeException as ex:
LOGGER.warning(ex)
@@ -56,7 +42,7 @@ def _capture_image(call: ServiceCall) -> None:
target_entities = [
entity_id
for entity_id in _get_abode_system(call.hass).entity_ids
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
if entity_id in entity_ids
]
@@ -71,7 +57,7 @@ def _trigger_automation(call: ServiceCall) -> None:
target_entities = [
entity_id
for entity_id in _get_abode_system(call.hass).entity_ids
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
if entity_id in entity_ids
]
+4 -3
View File
@@ -7,11 +7,12 @@ from typing import Any, cast
from jaraco.abode.devices.switch import Switch
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry
from .const import DOMAIN_DATA
from .entity import AbodeAutomation, AbodeDevice
DEVICE_TYPES = ["switch", "valve"]
@@ -19,11 +20,11 @@ DEVICE_TYPES = ["switch", "valve"]
async def async_setup_entry(
hass: HomeAssistant,
entry: AbodeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode switch devices."""
data = entry.runtime_data
data = hass.data[DOMAIN_DATA]
entities: list[SwitchEntity] = [
AbodeSwitch(data, device)
@@ -36,9 +36,7 @@ def _make_detected_condition(
) -> type[Condition]:
"""Create a detected condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
STATE_ON,
support_duration=True,
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
)
@@ -47,9 +45,7 @@ def _make_cleared_condition(
) -> type[Condition]:
"""Create a cleared condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
STATE_OFF,
support_duration=True,
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
)
@@ -249,11 +249,6 @@
.condition_binary_common: &condition_binary_common
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_gas_detected:
<<: *condition_binary_common
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -25,9 +24,6 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Carbon monoxide cleared"
@@ -37,9 +33,6 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Carbon monoxide detected"
@@ -61,9 +54,6 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Gas cleared"
@@ -73,9 +63,6 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Gas detected"
@@ -181,9 +168,6 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Smoke cleared"
@@ -193,9 +177,6 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Smoke detected"
@@ -4,7 +4,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
Condition,
EntityStateConditionBase,
make_entity_state_condition,
@@ -26,7 +25,6 @@ class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
"""State condition."""
_required_features: int
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain with the required features."""
@@ -84,11 +82,9 @@ CONDITIONS: dict[str, type[Condition]] = {
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"is_disarmed": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.DISARMED, support_duration=True
),
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
"is_triggered": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.TRIGGERED, support_duration=True
DOMAIN, AlarmControlPanelState.TRIGGERED
),
}
@@ -1,9 +1,9 @@
.condition_common: &condition_common
target: &condition_common_target
target:
entity:
domain: alarm_control_panel
fields: &condition_common_fields
behavior: &condition_common_behavior
behavior:
required: true
default: any
selector:
@@ -13,20 +13,10 @@
- all
- any
.condition_common_for: &condition_common_for
target: *condition_common_target
fields: &condition_common_for_fields
behavior: *condition_common_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_armed: *condition_common
is_armed_away:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
@@ -34,7 +24,7 @@ is_armed_away:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
is_armed_home:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
@@ -42,7 +32,7 @@ is_armed_home:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
is_armed_night:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
@@ -50,13 +40,13 @@ is_armed_night:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
is_armed_vacation:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
is_disarmed: *condition_common_for
is_disarmed: *condition_common
is_triggered: *condition_common_for
is_triggered: *condition_common
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -20,9 +19,6 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed away"
@@ -32,9 +28,6 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed home"
@@ -44,9 +37,6 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed night"
@@ -56,9 +46,6 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed vacation"
@@ -68,9 +55,6 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is disarmed"
@@ -80,9 +64,6 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is triggered"
@@ -43,6 +43,7 @@ from homeassistant.helpers.selector import (
from homeassistant.helpers.typing import VolDictType
from .const import (
CODE_EXECUTION_UNSUPPORTED_MODELS,
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
@@ -65,6 +66,7 @@ from .const import (
DOMAIN,
MIN_THINKING_BUDGET,
TOOL_SEARCH_UNSUPPORTED_MODELS,
WEB_SEARCH_UNSUPPORTED_MODELS,
PromptCaching,
)
from .coordinator import model_alias
@@ -387,6 +389,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
else cv.positive_int,
}
model = self.options[CONF_CHAT_MODEL]
if (
self.model_info.capabilities
and self.model_info.capabilities.thinking.supported
@@ -441,34 +445,43 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
else:
self.options.pop(CONF_THINKING_EFFORT, None)
step_schema.update(
{
if not model.startswith(tuple(CODE_EXECUTION_UNSUPPORTED_MODELS)):
step_schema[
vol.Optional(
CONF_CODE_EXECUTION,
default=DEFAULT[CONF_CODE_EXECUTION],
): bool,
vol.Optional(
CONF_WEB_SEARCH,
default=DEFAULT[CONF_WEB_SEARCH],
): bool,
vol.Optional(
CONF_WEB_SEARCH_MAX_USES,
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
): int,
vol.Optional(
CONF_WEB_SEARCH_USER_LOCATION,
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
): bool,
}
)
)
] = bool
else:
self.options.pop(CONF_CODE_EXECUTION, None)
if not model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
step_schema.update(
{
vol.Optional(
CONF_WEB_SEARCH,
default=DEFAULT[CONF_WEB_SEARCH],
): bool,
vol.Optional(
CONF_WEB_SEARCH_MAX_USES,
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
): int,
vol.Optional(
CONF_WEB_SEARCH_USER_LOCATION,
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
): bool,
}
)
else:
self.options.pop(CONF_WEB_SEARCH, None)
self.options.pop(CONF_WEB_SEARCH_MAX_USES, None)
self.options.pop(CONF_WEB_SEARCH_USER_LOCATION, None)
self.options.pop(CONF_WEB_SEARCH_CITY, None)
self.options.pop(CONF_WEB_SEARCH_REGION, None)
self.options.pop(CONF_WEB_SEARCH_COUNTRY, None)
self.options.pop(CONF_WEB_SEARCH_TIMEZONE, None)
model = self.options[CONF_CHAT_MODEL]
if not model.startswith(tuple(TOOL_SEARCH_UNSUPPORTED_MODELS)):
step_schema[
vol.Optional(
@@ -50,6 +50,15 @@ DEFAULT = {
CONF_WEB_SEARCH_MAX_USES: 5,
}
WEB_SEARCH_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
CODE_EXECUTION_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
TOOL_SEARCH_UNSUPPORTED_MODELS = [
"claude-3",
"claude-haiku",
]
@@ -28,7 +28,9 @@ _model_short_form = re.compile(r"[^\d]-\d$")
@callback
def model_alias(model_id: str) -> str:
"""Resolve alias from versioned model name."""
if model_id[-2:-1] != "-" and not model_id.endswith("-preview"):
if model_id == "claude-3-haiku-20240307" or model_id.endswith("-preview"):
return model_id
if model_id[-2:-1] != "-":
model_id = model_id[:-9]
if _model_short_form.search(model_id):
return model_id + "-0"
+1 -5
View File
@@ -124,14 +124,10 @@ def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
unsupported_keys = {"oneOf", "anyOf", "allOf"}
schema = convert(tool.parameters, custom_serializer=custom_serializer)
schema = {k: v for k, v in schema.items() if k not in unsupported_keys}
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=schema,
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
@@ -945,10 +945,7 @@ class PipelineRun:
try:
# Transcribe audio stream
stt_vad: VoiceCommandSegmenter | None = None
if (
self.audio_settings.is_vad_enabled
and self.stt_provider.audio_processing.requires_external_vad
):
if self.audio_settings.is_vad_enabled:
stt_vad = VoiceCommandSegmenter(
silence_seconds=self.audio_settings.silence_seconds
)
@@ -7,17 +7,13 @@ from .const import DOMAIN
from .entity import AssistSatelliteState
CONDITIONS: dict[str, type[Condition]] = {
"is_idle": make_entity_state_condition(
DOMAIN, AssistSatelliteState.IDLE, support_duration=True
),
"is_listening": make_entity_state_condition(
DOMAIN, AssistSatelliteState.LISTENING, support_duration=True
),
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
"is_processing": make_entity_state_condition(
DOMAIN, AssistSatelliteState.PROCESSING, support_duration=True
DOMAIN, AssistSatelliteState.PROCESSING
),
"is_responding": make_entity_state_condition(
DOMAIN, AssistSatelliteState.RESPONDING, support_duration=True
DOMAIN, AssistSatelliteState.RESPONDING
),
}
@@ -12,11 +12,6 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_idle: *condition_common
is_listening: *condition_common
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -11,9 +10,6 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is idle"
@@ -23,9 +19,6 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is listening"
@@ -35,9 +28,6 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is processing"
@@ -47,9 +37,6 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is responding"
@@ -169,7 +169,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"cover",
"device_tracker",
"door",
"doorbell",
"event",
"fan",
"garage_door",
+11 -22
View File
@@ -1,12 +1,8 @@
"""Support for Amazon Web Services (AWS)."""
from __future__ import annotations
import asyncio
from collections import OrderedDict
from dataclasses import dataclass
import logging
from typing import Any
from aiobotocore.session import AioSession
import voluptuous as vol
@@ -34,22 +30,14 @@ from .const import (
CONF_REGION,
CONF_SECRET_ACCESS_KEY,
CONF_VALIDATE,
DATA_AWS,
DATA_CONFIG,
DATA_HASS_CONFIG,
DATA_SESSIONS,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@dataclass
class AWSData:
"""Runtime data for the AWS integration."""
hass_config: ConfigType
config: dict[str, Any]
sessions: OrderedDict[str, AioSession]
AWS_CREDENTIAL_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
@@ -100,13 +88,14 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up AWS component."""
hass.data[DATA_HASS_CONFIG] = config
if (conf := config.get(DOMAIN)) is None:
# create a default conf using default profile
conf = CONFIG_SCHEMA({ATTR_CREDENTIALS: DEFAULT_CREDENTIAL})
hass.data[DATA_AWS] = AWSData(
hass_config=config, config=conf, sessions=OrderedDict()
)
hass.data[DATA_CONFIG] = conf
hass.data[DATA_SESSIONS] = OrderedDict()
hass.async_create_task(
hass.config_entries.flow.async_init(
@@ -122,8 +111,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Validate and save sessions per aws credential.
"""
data = hass.data[DATA_AWS]
conf = data.config
config = hass.data[DATA_HASS_CONFIG]
conf = hass.data[DATA_CONFIG]
if entry.source == config_entries.SOURCE_IMPORT:
if conf is None:
@@ -154,14 +143,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
validation = False
else:
data.sessions[name] = result
hass.data[DATA_SESSIONS][name] = result
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
for notify_config in conf[CONF_NOTIFY]:
hass.async_create_task(
discovery.async_load_platform(
hass, Platform.NOTIFY, DOMAIN, notify_config, data.hass_config
hass, Platform.NOTIFY, DOMAIN, notify_config, config
)
)
+3 -10
View File
@@ -1,17 +1,10 @@
"""Constant for AWS component."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import AWSData
DOMAIN = "aws"
DATA_AWS: HassKey[AWSData] = HassKey(DOMAIN)
DATA_CONFIG = "aws_config"
DATA_HASS_CONFIG = "aws_hass_config"
DATA_SESSIONS = "aws_sessions"
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_CONTEXT = "context"
+4 -6
View File
@@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_AWS
from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_SESSIONS
_LOGGER = logging.getLogger(__name__)
@@ -76,12 +76,10 @@ async def async_get_service(
if CONF_CONTEXT in aws_config:
del aws_config[CONF_CONTEXT]
sessions = hass.data[DATA_AWS].sessions
if not aws_config:
# no platform config, use the first aws component credential instead
if sessions:
session = next(iter(sessions.values()))
if hass.data[DATA_SESSIONS]:
session = next(iter(hass.data[DATA_SESSIONS].values()))
else:
_LOGGER.error("Missing aws credential for %s", config[CONF_NAME])
return None
@@ -89,7 +87,7 @@ async def async_get_service(
if session is None:
credential_name = aws_config.get(CONF_CREDENTIAL_NAME)
if credential_name is not None:
session = sessions.get(credential_name)
session = hass.data[DATA_SESSIONS].get(credential_name)
if session is None:
_LOGGER.warning("No available aws session for %s", credential_name)
del aws_config[CONF_CREDENTIAL_NAME]
@@ -5,7 +5,10 @@ from __future__ import annotations
import dataclasses
from typing import Any
from homeassistant.components.backup import DATA_MANAGER as BACKUP_DATA_MANAGER
from homeassistant.components.backup import (
DATA_MANAGER as BACKUP_DATA_MANAGER,
BackupManager,
)
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
@@ -28,7 +31,7 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
backup_manager = hass.data[BACKUP_DATA_MANAGER]
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
backups = await async_list_backups_from_s3(
coordinator.client,
bucket=entry.data[CONF_BUCKET],
+4 -10
View File
@@ -29,17 +29,11 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
}
CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
),
"is_level": make_entity_numerical_condition(
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
@@ -13,11 +13,6 @@
options:
- all
- any
for: &condition_for
required: true
default: 00:00:00
selector:
duration:
.battery_threshold_entity: &battery_threshold_entity
- domain: input_number
@@ -44,7 +39,6 @@ is_charging:
device_class: battery_charging
fields:
behavior: *condition_behavior
for: *condition_for
is_not_charging:
target:
@@ -53,7 +47,6 @@ is_not_charging:
device_class: battery_charging
fields:
behavior: *condition_behavior
for: *condition_for
is_level:
target:
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -13,9 +12,6 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is charging"
@@ -37,9 +33,6 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is low"
@@ -49,9 +42,6 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is not charging"
@@ -61,9 +51,6 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is not low"
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.1"],
"requirements": ["blebox-uniapi==2.5.0"],
"zeroconf": ["_bbxsrv._tcp.local."]
}
@@ -1,5 +1,4 @@
"""The Broadlink integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -34,8 +34,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink climate entities."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
if device.api.type in DOMAINS_AND_TYPES[Platform.CLIMATE]:
@@ -6,6 +6,7 @@ DOMAIN = "broadlink"
DOMAINS_AND_TYPES = {
Platform.CLIMATE: {"HYS"},
Platform.INFRARED: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
Platform.LIGHT: {"LB1", "LB2"},
Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
Platform.SELECT: {"HYS"},
@@ -44,3 +45,6 @@ DEVICE_TYPES = set.union(*DOMAINS_AND_TYPES.values())
DEFAULT_PORT = 80
DEFAULT_TIMEOUT = 5
# Broadlink IR packet format - repeat count byte offset
IR_PACKET_REPEAT_INDEX = 1
@@ -133,8 +133,6 @@ class BroadlinkDevice[_ApiT: blk.Device = blk.Device]:
await coordinator.async_config_entry_first_refresh()
self.update_manager = update_manager
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
self.hass.data[DOMAIN].devices[config.entry_id] = self
self.reset_jobs.append(config.add_update_listener(self.async_update))
@@ -0,0 +1,184 @@
"""Infrared platform for Broadlink remotes."""
from __future__ import annotations
from typing import TYPE_CHECKING
from broadlink.exceptions import BroadlinkException
from broadlink.remote import pulses_to_data as _bl_pulses_to_data
import infrared_protocols
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, IR_PACKET_REPEAT_INDEX
from .entity import BroadlinkEntity
if TYPE_CHECKING:
from .device import BroadlinkDevice
PARALLEL_UPDATES = 1
class BroadlinkIRCommand(InfraredCommand):
"""Raw IR command with optional Broadlink hardware repeat count.
This class lets you send raw timing data through a Broadlink infrared
entity. The repeat_count maps directly to the Broadlink packet repeat
byte: the device will re-transmit the entire IR burst that many
additional times after the first transmission.
Use this when you have existing Broadlink-encoded IR data (e.g. from
IR code databases like SmartIR) and want to use it with the new
infrared platform.
Protocol-aware commands (infrared_protocols.NECCommand, LgTVCommand,
etc.) manage repeats *inside* get_raw_timings() and should use the
default repeat=0. Only BroadlinkIRCommand should set hardware repeat.
Example: Migrating IR code database base64 codes to the infrared platform:
import base64
from broadlink.remote import data_to_pulses
from homeassistant.components.broadlink.infrared import BroadlinkIRCommand
from homeassistant.components.broadlink.const import IR_PACKET_REPEAT_INDEX
# Decode base64 IR code (e.g. from IR code database)
packet_data = base64.b64decode(b64_code)
repeat_count = packet_data[IR_PACKET_REPEAT_INDEX]
# Parse Broadlink packet to microsecond timings
pulses = data_to_pulses(packet_data)
timings = list(zip(pulses[::2], pulses[1::2]))
if len(pulses) % 2:
timings.append((pulses[-1], 0))
# Create command
cmd = BroadlinkIRCommand(timings, repeat_count=repeat_count)
await infrared.async_send_command(hass, entity_id, cmd)
"""
# Standard IR carrier frequency. Broadlink hardware handles the carrier
# internally, so this value is informational only.
MODULATION = 38000
def __init__(
self,
timings: list[tuple[int, int]],
repeat_count: int = 0,
) -> None:
"""Initialize with timing pairs and optional repeat count.
Args:
timings: List of (mark_us, space_us) pairs in microseconds.
repeat_count: Broadlink hardware repeat count (0 = send once).
Must be 0255 (the hardware repeat byte is a single unsigned byte).
Raises:
ValueError: If repeat_count is outside 0255 range.
"""
if not 0 <= repeat_count <= 255:
raise ValueError(f"repeat_count must be 0255, got {repeat_count}")
super().__init__(modulation=self.MODULATION, repeat_count=repeat_count)
self._timings = [
infrared_protocols.Timing(high_us=high, low_us=low) for high, low in timings
]
def get_raw_timings(self) -> list[infrared_protocols.Timing]:
"""Return timing pairs for transmission."""
return self._timings
def timings_to_broadlink_packet(
timings: list[tuple[int, int]],
repeat: int = 0,
) -> bytes:
"""Convert raw timing pairs (high_us, low_us) to a Broadlink IR packet.
Args:
timings: List of (mark_us, space_us) pairs in microseconds.
repeat: Number of extra repeats (0 = send once).
Returns:
Binary packet ready for Broadlink send_data().
"""
if not 0 <= repeat <= 255:
raise ValueError(f"repeat must be 0255, got {repeat}")
# Flatten (mark, space) pairs into a pulse list, omitting any zero-length spaces
pulses: list[int] = []
for high_us, low_us in timings:
pulses.append(high_us)
if low_us:
pulses.append(low_us)
# Use broadlink library's encoder (tick=32.84 µs)
packet = bytearray(_bl_pulses_to_data(pulses))
packet[IR_PACKET_REPEAT_INDEX] = repeat
return bytes(packet)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Broadlink infrared entity."""
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkInfraredEntity(device)])
class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEntity):
"""Broadlink infrared transmitter entity."""
_attr_has_entity_name = True
_attr_translation_key = "infrared"
def __init__(self, device: BroadlinkDevice) -> None:
"""Initialize the entity."""
super().__init__(device)
self._attr_unique_id = f"{device.unique_id}-infrared"
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command via the Broadlink device.
Handles two types of repeat behavior:
1. Protocol-aware commands (NECCommand, etc.): These encode repeats
(like NEC repeat codes) inside their get_raw_timings() data. The
Broadlink packet is sent with repeat=0.
2. BroadlinkIRCommand: Carries Broadlink hardware repeat count,
which tells the device to re-transmit the entire burst N times.
This is used for protocols/commands that need multiple full frame
transmissions (e.g. legacy SmartIR data).
Using isinstance check ensures protocol-level repeats (already in
timing data) don't get conflated with hardware repeats.
"""
timings = [
(timing.high_us, timing.low_us) for timing in command.get_raw_timings()
]
# Only BroadlinkIRCommand uses Broadlink hardware repeat. Protocol-aware
# commands (NECCommand, etc.) encode repeats inside get_raw_timings()
# and must use hardware repeat=0 to avoid double-repeating.
if isinstance(command, BroadlinkIRCommand):
repeat = command.repeat_count
else:
repeat = 0
packet = timings_to_broadlink_packet(timings, repeat=repeat)
try:
await self._device.async_request(self._device.api.send_data, packet)
except (BroadlinkException, OSError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_command_failed",
translation_placeholders={"error": str(err)},
) from err
@@ -32,8 +32,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink light."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
lights = []
@@ -3,6 +3,7 @@
"name": "Broadlink",
"codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am", "@eifinger"],
"config_flow": true,
"dependencies": ["infrared"],
"dhcp": [
{
"registered_devices": true
@@ -95,8 +95,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Broadlink remote."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
remote = BroadlinkRemote(
device,
@@ -31,8 +31,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink select."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkDayOfWeek(device)])
@@ -108,8 +108,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink sensor."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
sensor_data = device.update_manager.coordinator.data
sensors = [
@@ -49,6 +49,11 @@
}
},
"entity": {
"infrared": {
"infrared": {
"name": "IR transmitter"
}
},
"select": {
"day_of_week": {
"name": "Day of week",
@@ -77,5 +82,10 @@
"name": "Total consumption"
}
}
},
"exceptions": {
"send_command_failed": {
"message": "Failed to send IR command: {error}"
}
}
}
@@ -1,5 +1,4 @@
"""Support for Broadlink switches."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -22,8 +22,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink time."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkTime(device)])
@@ -7,9 +7,7 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_event_active": make_entity_state_condition(
DOMAIN, STATE_ON, support_duration=True
),
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
}
@@ -12,8 +12,3 @@ is_event_active:
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least"
"condition_behavior_name": "Condition passes if"
},
"conditions": {
"is_event_active": {
@@ -9,9 +8,6 @@
"fields": {
"behavior": {
"name": "[%key:component::calendar::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::calendar::common::condition_for_name%]"
}
},
"name": "Calendar event is active"
@@ -1,5 +1,4 @@
"""Component to embed Google Cast."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
-2
View File
@@ -65,8 +65,6 @@ class ChromecastInfo:
"""
cast_info = self.cast_info
if self.cast_info.cast_type is None or self.cast_info.manufacturer is None:
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
unknown_models = hass.data[DOMAIN]["unknown_models"]
if self.cast_info.model_name not in unknown_models:
# Manufacturer and cast type is not available in mDNS data,
@@ -1,5 +1,4 @@
"""Provide functionality to interact with Cast devices on the network."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -67,7 +67,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
CONDITIONS: dict[str, type[Condition]] = {
"is_hvac_mode": ClimateHVACModeCondition,
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
"is_on": make_entity_state_condition(
DOMAIN,
{
@@ -39,16 +39,7 @@
- domain: number
device_class: temperature
is_off:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_off: *condition_common
is_on: *condition_common
is_cooling: *condition_common
is_drying: *condition_common
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -53,9 +52,6 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Climate-control device is off"
@@ -15,7 +15,7 @@ from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
SerialPortSelector,
SerialSelector,
)
from .const import DOMAIN, LOGGER
@@ -110,7 +110,7 @@ class DenonRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
translation_key="model",
)
),
vol.Required(CONF_DEVICE): SerialPortSelector(),
vol.Required(CONF_DEVICE): SerialSelector(),
}
),
user_input or {},
@@ -1,5 +1,4 @@
"""Data used by this integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
-1
View File
@@ -1,5 +1,4 @@
"""Wrapper for media_source around async_upnp_client's DmsDevice ."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -1,15 +0,0 @@
"""Integration for doorbell triggers."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
DOMAIN = "doorbell"
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
__all__ = []
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
return True
@@ -1,7 +0,0 @@
{
"triggers": {
"rang": {
"trigger": "mdi:doorbell"
}
}
}
@@ -1,8 +0,0 @@
{
"domain": "doorbell",
"name": "Doorbell",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/doorbell",
"integration_type": "system",
"quality_scale": "internal"
}
@@ -1,9 +0,0 @@
{
"title": "Doorbell",
"triggers": {
"rang": {
"description": "Triggers after one or more doorbells rang.",
"name": "Doorbell rang"
}
}
}
@@ -1,50 +0,0 @@
"""Provides triggers for doorbells."""
from homeassistant.components.event import (
ATTR_EVENT_TYPE,
DOMAIN as EVENT_DOMAIN,
DoorbellEventType,
EventDeviceClass,
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
Trigger,
)
class DoorbellRangTrigger(EntityTriggerBase):
"""Trigger for doorbell event entity when a ring event is received."""
_domain_specs = {EVENT_DOMAIN: DomainSpec(device_class=EventDeviceClass.DOORBELL)}
_schema = ENTITY_STATE_TRIGGER_SCHEMA
def is_valid_state(self, state: State) -> bool:
"""Check if the entity is available and the event type is ring."""
return (
state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
and state.attributes.get(ATTR_EVENT_TYPE) == DoorbellEventType.RING
)
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and different from the current state."""
# UNKNOWN is a valid from_state, otherwise the first time the event is received
# would not trigger
if from_state.state == STATE_UNAVAILABLE:
return False
return from_state.state != to_state.state
TRIGGERS: dict[str, type[Trigger]] = {
"rang": DoorbellRangTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for doorbells."""
return TRIGGERS
@@ -1,5 +0,0 @@
rang:
target:
entity:
domain: event
device_class: doorbell
@@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
@@ -36,27 +35,6 @@ class DucoConfigFlow(ConfigFlow, domain=DOMAIN):
_host: str
_box_name: str
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
await self.async_set_unique_id(format_mac(discovery_info.macaddress))
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
try:
box_name, _ = await self._validate_input(discovery_info.ip)
except DucoConnectionError:
return self.async_abort(reason="cannot_connect")
except DucoError:
_LOGGER.exception("Unexpected error discovering Duco box via DHCP")
return self.async_abort(reason="unknown")
self._host = discovery_info.ip
self._box_name = box_name
self.context["title_placeholders"] = {"name": box_name}
return await self.async_step_discovery_confirm()
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
-1
View File
@@ -64,7 +64,6 @@ async def async_setup_entry(
"""Set up Duco fan entities."""
coordinator = entry.runtime_data
# BOX is always node 1 and is never dynamically added or removed, so no listener needed.
async_add_entities(
DucoVentilationFanEntity(coordinator, node)
for node in coordinator.data.nodes.values()
+2 -7
View File
@@ -3,17 +3,12 @@
"name": "Duco",
"codeowners": ["@ronaldvdmeer"],
"config_flow": true,
"dhcp": [
{
"hostname": "duco_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]"
}
],
"documentation": "https://www.home-assistant.io/integrations/duco",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.3.4"],
"quality_scale": "silver",
"requirements": ["python-duco-client==0.3.2"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
@@ -55,7 +55,11 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices:
status: todo
comment: >-
Users can pair new modules (CO2 sensors, humidity sensors, zone valves)
to their Duco box. Dynamic device support to be added in a follow-up PR.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -70,7 +74,11 @@ rules:
handled by the coordinator (unavailable entities) and resolve automatically.
There are no credentials to expire and no versioned API to become
incompatible with.
stale-devices: done
stale-devices:
status: todo
comment: >-
To be implemented together with dynamic device support in a follow-up PR.
# Platinum
async-dependency: done
inject-websession: done
+11 -43
View File
@@ -19,11 +19,9 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import DucoConfigEntry, DucoCoordinator
from .entity import DucoEntity
@@ -113,52 +111,22 @@ async def async_setup_entry(
"""Set up Duco sensor entities."""
coordinator = entry.runtime_data
# Track the node IDs for which entities have already been created, so we
# can detect both newly added and stale (deregistered) nodes on every
# coordinator update.
known_nodes: set[int] = set()
@callback
def _async_add_new_entities() -> None:
# Remove devices whose nodes have disappeared from the API.
# The firmware removes deregistered RF/wired nodes automatically.
# BSRH box sensors that are physically unplugged from the PCB are
# not deregistered by the firmware and will never appear here as stale.
stale_node_ids = known_nodes - coordinator.data.nodes.keys()
if stale_node_ids:
device_reg = dr.async_get(hass)
mac = entry.unique_id
for node_id in stale_node_ids:
device = device_reg.async_get_device(
identifiers={(DOMAIN, f"{mac}_{node_id}")}
)
if device:
device_reg.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
)
known_nodes.difference_update(stale_node_ids)
new_entities: list[SensorEntity] = []
for node in coordinator.data.nodes.values():
if node.node_id in known_nodes:
continue
known_nodes.add(node.node_id)
new_entities.extend(
async_add_entities(
[
*[
DucoSensorEntity(coordinator, node, description)
for node in coordinator.data.nodes.values()
for description in SENSOR_DESCRIPTIONS
if node.general.node_type in description.node_types
)
new_entities.extend(
],
*[
DucoBoxSensorEntity(coordinator, node, description)
for node in coordinator.data.nodes.values()
for description in BOX_SENSOR_DESCRIPTIONS
if node.general.node_type == NodeType.BOX
)
if new_entities:
async_add_entities(new_entities)
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_entities))
_async_add_new_entities()
],
]
)
class DucoSensorEntity(DucoEntity, SensorEntity):
@@ -1,5 +1,4 @@
"""The EARN-E P1 Meter integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -40,7 +40,5 @@ class DomainData:
@cache
def get(cls, hass: HomeAssistant) -> Self:
"""Get the global DomainData instance stored in hass.data."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
ret = hass.data[DOMAIN] = cls()
return ret
+5 -1
View File
@@ -35,7 +35,11 @@ class EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState], InfraredEn
@convert_api_error_ha_error
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command."""
timings = command.get_raw_timings()
timings = [
interval
for timing in command.get_raw_timings()
for interval in (timing.high_us, -timing.low_us)
]
_LOGGER.debug("Sending command: %s", timings)
self._client.infrared_rf_transmit_raw_timings(
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==44.18.0",
"aioesphomeapi==44.13.3",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.7.3"
],
@@ -11,7 +11,6 @@ from aioesphomeapi import (
WaterHeaterInfo,
WaterHeaterMode,
WaterHeaterState,
WaterHeaterStateFlag,
)
from homeassistant.components.water_heater import (
@@ -73,8 +72,6 @@ class EsphomeWaterHeater(
self._attr_operation_list = None
if static_info.supported_features & WaterHeaterFeature.SUPPORTS_ON_OFF:
features |= WaterHeaterEntityFeature.ON_OFF
if static_info.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE:
features |= WaterHeaterEntityFeature.AWAY_MODE
self._attr_supported_features = features
@property
@@ -95,12 +92,6 @@ class EsphomeWaterHeater(
"""Return current operation mode."""
return _WATER_HEATER_MODES.from_esphome(self._state.mode)
@property
@esphome_state_property
def is_away_mode_on(self) -> bool | None:
"""Return true if away mode is on."""
return bool(self._state.state & WaterHeaterStateFlag.AWAY)
@convert_api_error_ha_error
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
@@ -137,24 +128,6 @@ class EsphomeWaterHeater(
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_turn_away_mode_on(self) -> None:
"""Turn away mode on."""
self._client.water_heater_command(
key=self._key,
away=True,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_turn_away_mode_off(self) -> None:
"""Turn away mode off."""
self._client.water_heater_command(
key=self._key,
away=False,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
platform_async_setup_entry,
+2 -2
View File
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
@@ -12,11 +12,6 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_off: *condition_common
is_on: *condition_common
@@ -1,7 +1,6 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -11,9 +10,6 @@
"fields": {
"behavior": {
"name": "[%key:component::fan::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::fan::common::condition_for_name%]"
}
},
"name": "Fan is off"
@@ -23,9 +19,6 @@
"fields": {
"behavior": {
"name": "[%key:component::fan::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::fan::common::condition_for_name%]"
}
},
"name": "Fan is on"
@@ -1,9 +1,4 @@
{
"common": {
"api_key": "Access token",
"api_key_description": "The access token for authenticating with Firefly III",
"verify_ssl_description": "Verify the SSL certificate of the Firefly III instance"
},
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
@@ -19,39 +14,39 @@
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:component::firefly_iii::common::api_key%]"
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::firefly_iii::common::api_key_description%]"
"api_key": "The new API access token for authenticating with Firefly III"
},
"description": "The access token for your Firefly III instance is invalid and needs to be updated. Go to **Options > Remote access and tokens**. Create a new **personal access token** and copy it (it will only display once)."
"description": "The access token for your Firefly III instance is invalid and needs to be updated. Go to **Options > Remote access and tokens**. Create a new personal access token and copy it (it will only display once)."
},
"reconfigure": {
"data": {
"api_key": "[%key:component::firefly_iii::common::api_key%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "[%key:component::firefly_iii::common::api_key_description%]",
"api_key": "[%key:component::firefly_iii::config::step::user::data_description::api_key%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:component::firefly_iii::common::verify_ssl_description%]"
"verify_ssl": "[%key:component::firefly_iii::config::step::user::data_description::verify_ssl%]"
},
"description": "Use the following form to reconfigure your Firefly III instance.",
"title": "Reconfigure Firefly III Integration"
},
"user": {
"data": {
"api_key": "[%key:component::firefly_iii::common::api_key%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "[%key:component::firefly_iii::common::api_key_description%]",
"api_key": "The API key for authenticating with Firefly III",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:component::firefly_iii::common::verify_ssl_description%]"
"verify_ssl": "Verify the SSL certificate of the Firefly III instance"
},
"description": "You can create an access token in the Firefly III UI. Go to **Options > Remote access and tokens**. Create a new **personal access token** and copy it (it will only display once)."
"description": "You can create an API key in the Firefly III UI. Go to **Options > Remote access and tokens**. Create a new personal access token and copy it (it will only display once)."
}
}
},
@@ -1,5 +1,4 @@
"""The Flux LED/MagicLight integration discovery."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -2,14 +2,11 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, Concatenate
from typing import Any
from afsapi import (
AFSAPI,
FSApiError,
FSConnectionError,
FSNotImplementedError,
PlayCaps,
@@ -27,7 +24,6 @@ from homeassistant.components.media_player import (
RepeatMode,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
@@ -39,37 +35,6 @@ from .const import DOMAIN, MEDIA_CONTENT_ID_PRESET
_LOGGER = logging.getLogger(__name__)
def fs_command_exception_wrap[
_AFSAPIDeviceT: AFSAPIDevice,
**_P,
_R,
](
func: Callable[Concatenate[_AFSAPIDeviceT, _P], Awaitable[_R]],
) -> Callable[Concatenate[_AFSAPIDeviceT, _P], Coroutine[Any, Any, _R]]:
"""Wrap command methods and map API exceptions to HA errors."""
@wraps(func)
async def _wrap(self: _AFSAPIDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
try:
return await func(self, *args, **kwargs)
except FSConnectionError as err:
command = func.__name__.removeprefix("async_")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={"command": command},
) from err
except FSApiError as err:
command = func.__name__.removeprefix("async_")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"command": command, "message": str(err)},
) from err
return _wrap
async def async_setup_entry(
hass: HomeAssistant,
config_entry: FrontierSiliconConfigEntry,
@@ -307,74 +272,57 @@ class AFSAPIDevice(MediaPlayerEntity):
# Management actions
# power control
@fs_command_exception_wrap
async def async_turn_on(self) -> None:
"""Turn on the device."""
await self.fs_device.set_power(True)
@fs_command_exception_wrap
async def async_turn_off(self) -> None:
"""Turn off the device."""
await self.fs_device.set_power(False)
@fs_command_exception_wrap
async def async_media_play(self) -> None:
"""Send play command."""
if (await self.fs_device.get_play_state()) == PlayState.STOPPED:
# The 'play' command only seems to work when the current stream is paused.
# We need to send a 'stop' command instead to resume a stopped stream.
await self.fs_device.stop()
else:
await self.fs_device.play()
await self.fs_device.play()
@fs_command_exception_wrap
async def async_media_pause(self) -> None:
"""Send pause command."""
await self.fs_device.pause()
@fs_command_exception_wrap
async def async_media_stop(self) -> None:
"""Send stop command."""
await self.fs_device.stop()
@fs_command_exception_wrap
async def async_media_previous_track(self) -> None:
"""Send previous track command (results in rewind)."""
await self.fs_device.rewind()
@fs_command_exception_wrap
async def async_media_next_track(self) -> None:
"""Send next track command (results in fast-forward)."""
await self.fs_device.forward()
@fs_command_exception_wrap
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
await self.fs_device.set_mute(mute)
# volume
@fs_command_exception_wrap
async def async_volume_up(self) -> None:
"""Send volume up command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) + 1
await self.fs_device.set_volume(min(volume, self._max_volume or 1))
@fs_command_exception_wrap
async def async_volume_down(self) -> None:
"""Send volume down command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) - 1
await self.fs_device.set_volume(max(volume, 0))
@fs_command_exception_wrap
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume command."""
if self._max_volume: # Can't do anything sensible if not set
volume = int(volume * self._max_volume)
await self.fs_device.set_volume(volume)
@fs_command_exception_wrap
async def async_select_source(self, source: str) -> None:
"""Select input source."""
await self.fs_device.set_power(True)
@@ -384,7 +332,6 @@ class AFSAPIDevice(MediaPlayerEntity):
):
await self.fs_device.set_mode(mode)
@fs_command_exception_wrap
async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Select EQ Preset."""
if (
@@ -393,7 +340,6 @@ class AFSAPIDevice(MediaPlayerEntity):
):
await self.fs_device.set_eq_preset(mode)
@fs_command_exception_wrap
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set repeat mode."""
await self.fs_device.play_repeat(
@@ -404,12 +350,10 @@ class AFSAPIDevice(MediaPlayerEntity):
}.get(repeat, PlayRepeatMode.OFF)
)
@fs_command_exception_wrap
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set shuffle mode."""
await self.fs_device.set_play_shuffle(shuffle)
@fs_command_exception_wrap
async def async_media_seek(self, position: float) -> None:
"""Seek to a position in seconds."""
await self.fs_device.set_play_position(int(position * 1000))
@@ -425,7 +369,6 @@ class AFSAPIDevice(MediaPlayerEntity):
return await browse_node(self.fs_device, media_content_type, media_content_id)
@fs_command_exception_wrap
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
@@ -33,13 +33,5 @@
}
}
}
},
"exceptions": {
"api_error": {
"message": "Failed to execute {command}: {message}"
},
"connection_error": {
"message": "Failed to execute {command}: could not connect to device"
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import FumisConfigEntry, FumisDataUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: FumisConfigEntry) -> bool:
@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from fumis import (
@@ -22,7 +21,6 @@ from homeassistant.helpers.selector import (
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import DOMAIN, LOGGER
@@ -30,64 +28,6 @@ from .const import DOMAIN, LOGGER
class FumisFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Fumis config flow."""
_discovered_mac: str
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery of a Fumis WiRCU module."""
mac = discovery_info.macaddress.replace(":", "").replace("-", "").upper()
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured()
self._discovered_mac = mac
return await self.async_step_dhcp_confirm()
async def async_step_dhcp_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle DHCP discovery confirmation."""
errors: dict[str, str] = {}
if user_input is not None:
fumis = Fumis(
mac=self._discovered_mac,
password=user_input[CONF_PIN],
session=async_get_clientsession(self.hass),
)
try:
info = await fumis.update_info()
except FumisAuthenticationError:
errors[CONF_PIN] = "invalid_auth"
except FumisStoveOfflineError:
errors["base"] = "device_offline"
except FumisConnectionError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=info.controller.model_name or "Fumis",
data={
CONF_MAC: self._discovered_mac,
CONF_PIN: user_input[CONF_PIN],
},
)
return self.async_show_form(
step_id="dhcp_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PIN): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -140,51 +80,3 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN):
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication of a Fumis stove."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-authentication confirmation."""
errors: dict[str, str] = {}
if user_input is not None:
reauth_entry = self._get_reauth_entry()
fumis = Fumis(
mac=reauth_entry.data[CONF_MAC],
password=user_input[CONF_PIN],
session=async_get_clientsession(self.hass),
)
try:
await fumis.update_info()
except FumisAuthenticationError:
errors[CONF_PIN] = "invalid_auth"
except FumisStoveOfflineError:
errors["base"] = "device_offline"
except FumisConnectionError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={CONF_PIN: user_input[CONF_PIN]},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PIN): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
),
errors=errors,
)
@@ -14,7 +14,6 @@ from fumis import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -48,7 +47,7 @@ class FumisDataUpdateCoordinator(DataUpdateCoordinator[FumisInfo]):
try:
return await self.client.update_info()
except FumisAuthenticationError as err:
raise ConfigEntryAuthFailed(
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="authentication_error",
) from err
-48
View File
@@ -1,48 +0,0 @@
{
"entity": {
"sensor": {
"combustion_chamber_temperature": {
"default": "mdi:thermometer-high"
},
"detailed_stove_status": {
"default": "mdi:fireplace"
},
"fan_1_speed": {
"default": "mdi:fan"
},
"fan_2_speed": {
"default": "mdi:fan"
},
"fuel_quantity": {
"default": "mdi:gauge"
},
"fuel_used": {
"default": "mdi:counter"
},
"igniter_starts": {
"default": "mdi:counter"
},
"misfires": {
"default": "mdi:alert-outline"
},
"overheatings": {
"default": "mdi:thermometer-alert"
},
"power_output": {
"default": "mdi:fire"
},
"pressure": {
"default": "mdi:gauge"
},
"stove_status": {
"default": "mdi:fireplace"
},
"time_to_service": {
"default": "mdi:wrench-clock"
},
"wifi_signal_strength": {
"default": "mdi:wifi"
}
}
}
}
@@ -3,11 +3,6 @@
"name": "Fumis",
"codeowners": ["@frenck"],
"config_flow": true,
"dhcp": [
{
"macaddress": "0016D0*"
}
],
"documentation": "https://www.home-assistant.io/integrations/fumis",
"integration_type": "device",
"iot_class": "cloud_polling",
@@ -36,16 +36,18 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery: done
discovery:
status: todo
comment: DHCP discovery can be added.
discovery-update-info:
status: exempt
comment: Cloud-only API, no local device information to update.
status: todo
comment: DHCP discovery based update can be added.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
-276
View File
@@ -1,276 +0,0 @@
"""Support for Fumis sensor entities."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from fumis import FumisInfo, StoveState, StoveStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .coordinator import FumisConfigEntry, FumisDataUpdateCoordinator
from .entity import FumisEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class FumisSensorEntityDescription(SensorEntityDescription):
"""Describes a Fumis sensor entity."""
has_fn: Callable[[FumisInfo], bool] = lambda _: True
value_fn: Callable[[FumisInfo], datetime | float | int | str | None]
SENSORS: tuple[FumisSensorEntityDescription, ...] = (
FumisSensorEntityDescription(
key="combustion_chamber_temperature",
translation_key="combustion_chamber_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.controller.combustion_chamber_temperature is not None,
value_fn=lambda data: data.controller.combustion_chamber_temperature,
),
FumisSensorEntityDescription(
key="detailed_stove_status",
translation_key="detailed_stove_status",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
status.name.lower()
for status in StoveStatus
if status != StoveStatus.UNKNOWN
],
value_fn=lambda data: (
None
if data.controller.stove_status is StoveStatus.UNKNOWN
else data.controller.stove_status.name.lower()
),
),
FumisSensorEntityDescription(
key="fan_1_speed",
translation_key="fan_1_speed",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.controller.fan1_speed is not None,
value_fn=lambda data: data.controller.fan1_speed,
),
FumisSensorEntityDescription(
key="fan_2_speed",
translation_key="fan_2_speed",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.controller.fan2_speed is not None,
value_fn=lambda data: data.controller.fan2_speed,
),
FumisSensorEntityDescription(
key="fuel_quantity",
translation_key="fuel_quantity",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: (
len(data.controller.fuels) > 0
and data.controller.fuels[0].quantity_percentage is not None
),
value_fn=lambda data: (
data.controller.fuels[0].quantity_percentage
if data.controller.fuels
else None
),
),
FumisSensorEntityDescription(
key="fuel_used",
translation_key="fuel_used",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.fuel_quantity_used,
),
FumisSensorEntityDescription(
key="heating_time",
translation_key="heating_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_unit_of_measurement=UnitOfTime.HOURS,
value_fn=lambda data: data.controller.statistic.heating_time.total_seconds(),
),
FumisSensorEntityDescription(
key="igniter_starts",
translation_key="igniter_starts",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.igniter_starts,
),
FumisSensorEntityDescription(
key="misfires",
translation_key="misfires",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.misfires,
),
FumisSensorEntityDescription(
key="module_temperature",
translation_key="module_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.unit.temperature is not None,
value_fn=lambda data: data.unit.temperature,
),
FumisSensorEntityDescription(
key="overheatings",
translation_key="overheatings",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.overheatings,
),
FumisSensorEntityDescription(
key="power_output",
translation_key="power_output",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
value_fn=lambda data: data.controller.power.kw,
),
FumisSensorEntityDescription(
key="pressure",
translation_key="pressure",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.controller.pressure is not None,
value_fn=lambda data: data.controller.pressure,
),
FumisSensorEntityDescription(
key="stove_status",
translation_key="stove_status",
device_class=SensorDeviceClass.ENUM,
options=[state.value for state in StoveState if state != StoveState.UNKNOWN],
value_fn=lambda data: (
None
if data.controller.state is StoveState.UNKNOWN
else data.controller.state.value
),
),
FumisSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
has_fn=lambda data: data.controller.main_temperature is not None,
value_fn=lambda data: (
data.controller.main_temperature.actual
if data.controller.main_temperature
else None
),
),
FumisSensorEntityDescription(
key="time_to_service",
translation_key="time_to_service",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: (
data.controller.time_to_service is not None
and data.controller.time_to_service >= 0
),
value_fn=lambda data: data.controller.time_to_service,
),
FumisSensorEntityDescription(
key="uptime",
translation_key="uptime",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=ignore_variance(
lambda data: (
utcnow().replace(microsecond=0) - data.controller.statistic.uptime
),
timedelta(minutes=5),
),
),
FumisSensorEntityDescription(
key="wifi_rssi",
translation_key="wifi_rssi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda data: data.unit.rssi,
),
FumisSensorEntityDescription(
key="wifi_signal_strength",
translation_key="wifi_signal_strength",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.unit.signal_strength,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FumisConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fumis sensor entities based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
FumisSensorEntity(coordinator=coordinator, description=description)
for description in SENSORS
if description.has_fn(coordinator.data)
)
class FumisSensorEntity(FumisEntity, SensorEntity):
"""Defines a Fumis sensor entity."""
entity_description: FumisSensorEntityDescription
def __init__(
self,
coordinator: FumisDataUpdateCoordinator,
description: FumisSensorEntityDescription,
) -> None:
"""Initialize the Fumis sensor 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) -> datetime | float | int | str | None:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.data)
+1 -102
View File
@@ -1,8 +1,7 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -11,24 +10,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"dhcp_confirm": {
"data": {
"pin": "[%key:component::fumis::config::step::user::data::pin%]"
},
"data_description": {
"pin": "[%key:component::fumis::config::step::user::data_description::pin%]"
},
"description": "A Fumis WiRCU Wi-Fi module was discovered on your network. Enter the PIN code from the label on the module to set up your pellet stove."
},
"reauth_confirm": {
"data": {
"pin": "[%key:component::fumis::config::step::user::data::pin%]"
},
"data_description": {
"pin": "[%key:component::fumis::config::step::user::data_description::pin%]"
},
"description": "The PIN code for your stove has changed. Please enter the new PIN code to re-authenticate."
},
"user": {
"data": {
"mac": "MAC address",
@@ -42,88 +23,6 @@
}
}
},
"entity": {
"sensor": {
"combustion_chamber_temperature": {
"name": "Combustion chamber"
},
"detailed_stove_status": {
"name": "Detailed stove status",
"state": {
"cold_start": "Cold start",
"cold_start_off": "Off (cold start)",
"combustion": "Combustion",
"cooling": "Cooling",
"eco": "Eco",
"hybrid_init": "Hybrid init",
"hybrid_start": "Hybrid start",
"ignition": "Ignition",
"off": "[%key:common::state::off%]",
"pre_combustion": "Pre-combustion",
"pre_heating": "Pre-heating",
"wood_burning_off": "Off (wood burning)",
"wood_combustion": "Wood combustion",
"wood_start": "Wood start"
}
},
"fan_1_speed": {
"name": "Fan 1 speed"
},
"fan_2_speed": {
"name": "Fan 2 speed"
},
"fuel_quantity": {
"name": "Fuel level"
},
"fuel_used": {
"name": "Fuel consumed"
},
"heating_time": {
"name": "Burning time"
},
"igniter_starts": {
"name": "Igniter starts"
},
"misfires": {
"name": "Misfires"
},
"module_temperature": {
"name": "WiRCU module"
},
"overheatings": {
"name": "Overheatings"
},
"power_output": {
"name": "Power output"
},
"pressure": {
"name": "Combustion chamber pressure"
},
"stove_status": {
"name": "Stove status",
"state": {
"burning": "Burning",
"cooling": "Cooling",
"eco": "Eco",
"heating_up": "Heating up",
"ignition": "Ignition",
"off": "[%key:common::state::off%]"
}
},
"time_to_service": {
"name": "Time to service"
},
"uptime": {
"name": "Uptime"
},
"wifi_rssi": {
"name": "Wi-Fi RSSI"
},
"wifi_signal_strength": {
"name": "Wi-Fi signal strength"
}
}
},
"exceptions": {
"authentication_error": {
"message": "Authentication with the Fumis online service failed. Check your MAC address and PIN code."
@@ -1,5 +1,4 @@
"""Support for Actions on Google Assistant Smart Home Control."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

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