Compare commits

..

2 Commits

Author SHA1 Message Date
farmio
a166da552b fix type 2026-01-13 22:31:56 +01:00
farmio
31ae6951db KNX Expose: Add support for sending value periodically 2026-01-13 22:16:57 +01:00
1077 changed files with 2475 additions and 18386 deletions

View File

@@ -1024,6 +1024,18 @@ class MyCoordinator(DataUpdateCoordinator[MyData]):
)
```
### Entity Performance Optimization
```python
# Use __slots__ for memory efficiency
class MySensor(SensorEntity):
__slots__ = ("_attr_native_value", "_attr_available")
@property
def should_poll(self) -> bool:
"""Disable polling when using coordinator."""
return False # ✅ Let coordinator handle updates
```
## Testing Patterns
### Testing Best Practices
@@ -1169,4 +1181,4 @@ python -m script.hassfest --integration-path homeassistant/components/my_integra
pytest ./tests/components/my_integration \
--cov=homeassistant.components.my_integration \
--cov-report term-missing
```
```

View File

@@ -247,14 +247,20 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- *checkout
- name: Register problem matchers
- name: Register yamllint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
- name: Register check-json problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-json.json"
- name: Register check executables problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
- name: Register codespell problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # v1.0.12
uses: j178/prek-action@91fd7d7cf70ae1dee9f4f44e7dfa5d1073fe6623 # v1.0.11
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config
RUFF_OUTPUT_FORMAT: github

View File

@@ -4,7 +4,7 @@
"owner": "check-executables-have-shebangs",
"pattern": [
{
"regexp": "^(.+):\\s(marked executable but has no \\(or invalid\\) shebang!.*)$",
"regexp": "^(.+):\\s(.+)$",
"file": 1,
"message": 2
}

View File

@@ -39,7 +39,7 @@ repos:
- id: prettier
additional_dependencies:
- prettier@3.6.2
- prettier-plugin-sort-json@4.2.0
- prettier-plugin-sort-json@4.1.1
- repo: https://github.com/cdce8p/python-typing-update
rev: v0.6.0
hooks:

View File

@@ -407,7 +407,6 @@ homeassistant.components.person.*
homeassistant.components.pi_hole.*
homeassistant.components.ping.*
homeassistant.components.plugwise.*
homeassistant.components.pooldose.*
homeassistant.components.portainer.*
homeassistant.components.powerfox.*
homeassistant.components.powerwall.*

2
.vscode/tasks.json vendored
View File

@@ -120,7 +120,7 @@
{
"label": "Generate Requirements",
"type": "shell",
"command": "${command:python.interpreterPath} -m script.gen_requirements_all",
"command": "./script/gen_requirements_all.py",
"group": {
"kind": "build",
"isDefault": true

View File

@@ -1,93 +0,0 @@
"""Provides conditions for alarm control panels."""
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.condition import (
Condition,
EntityStateConditionBase,
make_entity_state_condition,
)
from homeassistant.helpers.entity import get_supported_features
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Test if an entity supports the specified features."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
return False
class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
"""State condition."""
_required_features: int
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain with the required features."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if supports_feature(self._hass, entity_id, self._required_features)
}
def make_entity_state_required_features_condition(
domain: str, to_state: str, required_features: int
) -> type[EntityStateRequiredFeaturesCondition]:
"""Create an entity state condition class with required feature filtering."""
class CustomCondition(EntityStateRequiredFeaturesCondition):
"""Condition for entity state changes."""
_domain = domain
_states = {to_state}
_required_features = required_features
return CustomCondition
CONDITIONS: dict[str, type[Condition]] = {
"is_armed": make_entity_state_condition(
DOMAIN,
{
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION,
},
),
"is_armed_away": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelEntityFeature.ARM_AWAY,
),
"is_armed_home": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelEntityFeature.ARM_HOME,
),
"is_armed_night": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelEntityFeature.ARM_NIGHT,
),
"is_armed_vacation": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
"is_triggered": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.TRIGGERED
),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the alarm control panel conditions."""
return CONDITIONS

View File

@@ -1,52 +0,0 @@
.condition_common: &condition_common
target:
entity:
domain: alarm_control_panel
fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_armed: *condition_common
is_armed_away:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
is_armed_home:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
is_armed_night:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
is_armed_vacation:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
is_disarmed: *condition_common
is_triggered: *condition_common

View File

@@ -1,27 +1,4 @@
{
"conditions": {
"is_armed": {
"condition": "mdi:shield"
},
"is_armed_away": {
"condition": "mdi:shield-lock"
},
"is_armed_home": {
"condition": "mdi:shield-home"
},
"is_armed_night": {
"condition": "mdi:shield-moon"
},
"is_armed_vacation": {
"condition": "mdi:shield-airplane"
},
"is_disarmed": {
"condition": "mdi:shield-off"
},
"is_triggered": {
"condition": "mdi:bell-ring"
}
},
"entity_component": {
"_": {
"default": "mdi:shield",

View File

@@ -1,82 +1,8 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted alarms.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_armed": {
"description": "Tests if one or more alarms are armed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed"
},
"is_armed_away": {
"description": "Tests if one or more alarms are armed in away mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed away"
},
"is_armed_home": {
"description": "Tests if one or more alarms are armed in home mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed home"
},
"is_armed_night": {
"description": "Tests if one or more alarms are armed in night mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed night"
},
"is_armed_vacation": {
"description": "Tests if one or more alarms are armed in vacation mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed vacation"
},
"is_disarmed": {
"description": "Tests if one or more alarms are disarmed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is disarmed"
},
"is_triggered": {
"description": "Tests if one or more alarms are triggered.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is triggered"
}
},
"device_automation": {
"action_type": {
"arm_away": "Arm {entity_name} away",
@@ -150,12 +76,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -14,7 +14,7 @@ from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelStat
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Test if an entity supports the specified features."""
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
@@ -39,7 +39,7 @@ class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
def make_entity_state_trigger_required_features(
domain: str, to_state: str, required_features: int
) -> type[EntityTargetStateTriggerBase]:
"""Create an entity state trigger class with required feature filtering."""
"""Create an entity state trigger class."""
class CustomTrigger(EntityStateTriggerRequiredFeatures):
"""Trigger for entity state changes."""

View File

@@ -1,23 +0,0 @@
"""Provides conditions for assist satellites."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from .const import DOMAIN
from .entity import AssistSatelliteState
CONDITIONS: dict[str, type[Condition]] = {
"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
),
"is_responding": make_entity_state_condition(
DOMAIN, AssistSatelliteState.RESPONDING
),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the assist satellite conditions."""
return CONDITIONS

View File

@@ -1,19 +0,0 @@
.condition_common: &condition_common
target:
entity:
domain: assist_satellite
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_idle: *condition_common
is_listening: *condition_common
is_processing: *condition_common
is_responding: *condition_common

View File

@@ -1,18 +1,4 @@
{
"conditions": {
"is_idle": {
"condition": "mdi:chat-sleep"
},
"is_listening": {
"condition": "mdi:chat-question"
},
"is_processing": {
"condition": "mdi:chat-processing"
},
"is_responding": {
"condition": "mdi:chat-alert"
}
},
"entity_component": {
"_": {
"default": "mdi:account-voice"

View File

@@ -1,52 +1,8 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted Assist satellites.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_idle": {
"description": "Tests if one or more Assist satellites are idle.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is idle"
},
"is_listening": {
"description": "Tests if one or more Assist satellites are listening.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is listening"
},
"is_processing": {
"description": "Tests if one or more Assist satellites are processing.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is processing"
},
"is_responding": {
"description": "Tests if one or more Assist satellites are responding.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is responding"
}
},
"entity_component": {
"_": {
"name": "Assist satellite",
@@ -65,12 +21,6 @@
"sentences": "Sentences"
}
},
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -123,9 +123,6 @@ SERVICE_TRIGGER = "trigger"
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"fan",
"light",
}

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import json
import logging
from typing import Any
from azure.servicebus import ServiceBusMessage
from azure.servicebus.aio import ServiceBusClient, ServiceBusSender
@@ -93,7 +92,7 @@ class ServiceBusNotificationService(BaseNotificationService):
"""Initialize the service."""
self._client = client
async def async_send_message(self, message: str, **kwargs: Any) -> None:
async def async_send_message(self, message, **kwargs):
"""Send a message."""
dto = {ATTR_ASB_MESSAGE: message}

View File

@@ -176,9 +176,6 @@
}
},
"triggers": {
"door.opened": {
"trigger": "mdi:door-open"
},
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},

View File

@@ -332,16 +332,6 @@
},
"title": "Binary sensor",
"triggers": {
"door.opened": {
"description": "Triggers after one or more occupancy doors open.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Door opened"
},
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {

View File

@@ -53,7 +53,6 @@ def make_binary_sensor_trigger(
TRIGGERS: dict[str, type[Trigger]] = {
"_door.opened": make_binary_sensor_trigger(BinarySensorDeviceClass.DOOR, STATE_ON),
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),

View File

@@ -23,10 +23,3 @@ occupancy_detected:
entity:
domain: binary_sensor
device_class: occupancy
_door.opened:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: door

View File

@@ -1,7 +1,5 @@
"""BleBox sensor entities."""
from datetime import datetime
import blebox_uniapi.sensor
from homeassistant.components.sensor import (
@@ -148,7 +146,7 @@ class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEn
return self._feature.native_value
@property
def last_reset(self) -> datetime | None:
def last_reset(self):
"""Return the time when the sensor was last reset, if implemented."""
native_implementation = getattr(self._feature, "last_reset", None)

View File

@@ -64,7 +64,6 @@ def _ws_with_blueprint_domain(
return with_domain_blueprints
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/list",
@@ -98,7 +97,6 @@ async def ws_list_blueprints(
connection.send_result(msg["id"], results)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/import",
@@ -152,7 +150,6 @@ async def ws_import_blueprint(
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/save",
@@ -209,7 +206,6 @@ async def ws_save_blueprint(
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/delete",
@@ -237,7 +233,6 @@ async def ws_delete_blueprint(
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/substitute",

View File

@@ -15,13 +15,5 @@
"get_events": {
"service": "mdi:calendar-month"
}
},
"triggers": {
"event_ended": {
"trigger": "mdi:calendar-end"
},
"event_started": {
"trigger": "mdi:calendar-start"
}
}
}

View File

@@ -45,14 +45,6 @@
"title": "Detected use of deprecated action calendar.list_events"
}
},
"selector": {
"trigger_offset_type": {
"options": {
"after": "After",
"before": "Before"
}
}
},
"services": {
"create_event": {
"description": "Adds a new calendar event.",
@@ -111,35 +103,5 @@
"name": "Get events"
}
},
"title": "Calendar",
"triggers": {
"event_ended": {
"description": "Triggers when a calendar event ends.",
"fields": {
"offset": {
"description": "Offset from the end of the event.",
"name": "Offset"
},
"offset_type": {
"description": "Whether to trigger before or after the end of the event, if an offset is defined.",
"name": "Offset type"
}
},
"name": "Calendar event ended"
},
"event_started": {
"description": "Triggers when a calendar event starts.",
"fields": {
"offset": {
"description": "Offset from the start of the event.",
"name": "Offset"
},
"offset_type": {
"description": "Whether to trigger before or after the start of the event, if an offset is defined.",
"name": "Offset type"
}
},
"name": "Calendar event started"
}
}
"title": "Calendar"
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import datetime
@@ -11,15 +10,8 @@ from typing import TYPE_CHECKING, Any, cast
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_ENTITY_ID,
CONF_EVENT,
CONF_OFFSET,
CONF_OPTIONS,
CONF_TARGET,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_OPTIONS
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
@@ -28,13 +20,12 @@ from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_time_interval,
)
from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from . import CalendarEntity, CalendarEvent
from .const import DATA_COMPONENT, DOMAIN
from .const import DATA_COMPONENT
_LOGGER = logging.getLogger(__name__)
@@ -42,35 +33,19 @@ EVENT_START = "start"
EVENT_END = "end"
UPDATE_INTERVAL = datetime.timedelta(minutes=15)
CONF_OFFSET_TYPE = "offset_type"
OFFSET_TYPE_BEFORE = "before"
OFFSET_TYPE_AFTER = "after"
_SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA = {
_OPTIONS_SCHEMA_DICT = {
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}),
vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
}
_SINGLE_ENTITY_EVENT_TRIGGER_SCHEMA = vol.Schema(
_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS): _SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA,
vol.Required(CONF_OPTIONS): _OPTIONS_SCHEMA_DICT,
},
)
_EVENT_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS, default={}): {
vol.Required(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
vol.Required(CONF_OFFSET_TYPE, default=OFFSET_TYPE_BEFORE): vol.In(
{OFFSET_TYPE_BEFORE, OFFSET_TYPE_AFTER}
),
},
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
}
)
# mypy: disallow-any-generics
@@ -80,7 +55,6 @@ class QueuedCalendarEvent:
trigger_time: datetime.datetime
event: CalendarEvent
entity_id: str
@dataclass
@@ -120,7 +94,7 @@ class Timespan:
return f"[{self.start}, {self.end})"
type EventFetcher = Callable[[Timespan], Awaitable[list[tuple[str, CalendarEvent]]]]
type EventFetcher = Callable[[Timespan], Awaitable[list[CalendarEvent]]]
type QueuedEventFetcher = Callable[[Timespan], Awaitable[list[QueuedCalendarEvent]]]
@@ -136,24 +110,15 @@ def get_entity(hass: HomeAssistant, entity_id: str) -> CalendarEntity:
return entity
def event_fetcher(hass: HomeAssistant, entity_ids: set[str]) -> EventFetcher:
def event_fetcher(hass: HomeAssistant, entity_id: str) -> EventFetcher:
"""Build an async_get_events wrapper to fetch events during a time span."""
async def async_get_events(timespan: Timespan) -> list[tuple[str, CalendarEvent]]:
async def async_get_events(timespan: Timespan) -> list[CalendarEvent]:
"""Return events active in the specified time span."""
entity = get_entity(hass, entity_id)
# Expand by one second to make the end time exclusive
end_time = timespan.end + datetime.timedelta(seconds=1)
events: list[tuple[str, CalendarEvent]] = []
for entity_id in entity_ids:
entity = get_entity(hass, entity_id)
events.extend(
(entity_id, event)
for event in await entity.async_get_events(
hass, timespan.start, end_time
)
)
return events
return await entity.async_get_events(hass, timespan.start, end_time)
return async_get_events
@@ -177,11 +142,12 @@ def queued_event_fetcher(
# Example: For an EVENT_END trigger the event may start during this
# time span, but need to be triggered later when the end happens.
results = []
for entity_id, event in active_events:
trigger_time = get_trigger_time(event)
for trigger_time, event in zip(
map(get_trigger_time, active_events), active_events, strict=False
):
if trigger_time not in offset_timespan:
continue
results.append(QueuedCalendarEvent(trigger_time + offset, event, entity_id))
results.append(QueuedCalendarEvent(trigger_time + offset, event))
_LOGGER.debug(
"Scan events @ %s%s found %s eligible of %s active",
@@ -274,7 +240,6 @@ class CalendarEventListener:
_LOGGER.debug("Dispatching event: %s", queued_event.event)
payload = {
**self._trigger_payload,
ATTR_ENTITY_ID: queued_event.entity_id,
"calendar_event": queued_event.event.as_dict(),
}
self._action_runner(payload, "calendar event state change")
@@ -295,77 +260,8 @@ class CalendarEventListener:
self._listen_next_calendar_event()
class TargetCalendarEventListener(TargetEntityChangeTracker):
"""Helper class to listen to calendar events for target entity changes."""
def __init__(
self,
hass: HomeAssistant,
target_selection: TargetSelection,
event_type: str,
offset: datetime.timedelta,
run_action: TriggerActionRunner,
) -> None:
"""Initialize the state change tracker."""
def entity_filter(entities: set[str]) -> set[str]:
return {
entity_id
for entity_id in entities
if split_entity_id(entity_id)[0] == DOMAIN
}
super().__init__(hass, target_selection, entity_filter)
self._event_type = event_type
self._offset = offset
self._run_action = run_action
self._trigger_data = {
"event": event_type,
"offset": offset,
}
self._pending_listener_task: asyncio.Task[None] | None = None
self._calendar_event_listener: CalendarEventListener | None = None
@callback
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
"""Restart the listeners when the list of entities of the tracked targets is updated."""
if self._pending_listener_task:
self._pending_listener_task.cancel()
self._pending_listener_task = self._hass.async_create_task(
self._start_listening(tracked_entities)
)
async def _start_listening(self, tracked_entities: set[str]) -> None:
"""Start listening for calendar events."""
_LOGGER.debug("Tracking events for calendars: %s", tracked_entities)
if self._calendar_event_listener:
self._calendar_event_listener.async_detach()
self._calendar_event_listener = CalendarEventListener(
self._hass,
self._run_action,
self._trigger_data,
queued_event_fetcher(
event_fetcher(self._hass, tracked_entities),
self._event_type,
self._offset,
),
)
await self._calendar_event_listener.async_attach()
def _unsubscribe(self) -> None:
"""Unsubscribe from all events."""
super()._unsubscribe()
if self._pending_listener_task:
self._pending_listener_task.cancel()
self._pending_listener_task = None
if self._calendar_event_listener:
self._calendar_event_listener.async_detach()
self._calendar_event_listener = None
class SingleEntityEventTrigger(Trigger):
"""Legacy single calendar entity event trigger."""
class EventTrigger(Trigger):
"""Calendar event trigger."""
_options: dict[str, Any]
@@ -375,7 +271,7 @@ class SingleEntityEventTrigger(Trigger):
) -> ConfigType:
"""Validate complete config."""
complete_config = move_top_level_schema_fields_to_options(
complete_config, _SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA
complete_config, _OPTIONS_SCHEMA_DICT
)
return await super().async_validate_complete_config(hass, complete_config)
@@ -384,7 +280,7 @@ class SingleEntityEventTrigger(Trigger):
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, _SINGLE_ENTITY_EVENT_TRIGGER_SCHEMA(config))
return cast(ConfigType, _CONFIG_SCHEMA(config))
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
@@ -415,72 +311,15 @@ class SingleEntityEventTrigger(Trigger):
run_action,
trigger_data,
queued_event_fetcher(
event_fetcher(self._hass, {entity_id}), event_type, offset
event_fetcher(self._hass, entity_id), event_type, offset
),
)
await listener.async_attach()
return listener.async_detach
class EventTrigger(Trigger):
"""Calendar event trigger."""
_options: dict[str, Any]
_event_type: str
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, _EVENT_TRIGGER_SCHEMA(config))
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.target is not None
assert config.options is not None
self._target = config.target
self._options = config.options
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
offset = self._options[CONF_OFFSET]
offset_type = self._options[CONF_OFFSET_TYPE]
if offset_type == OFFSET_TYPE_BEFORE:
offset = -offset
target_selection = TargetSelection(self._target)
if not target_selection.has_any_target:
raise HomeAssistantError(f"No target defined in {self._target}")
listener = TargetCalendarEventListener(
self._hass, target_selection, self._event_type, offset, run_action
)
return listener.async_setup()
class EventStartedTrigger(EventTrigger):
"""Calendar event started trigger."""
_event_type = EVENT_START
class EventEndedTrigger(EventTrigger):
"""Calendar event ended trigger."""
_event_type = EVENT_END
TRIGGERS: dict[str, type[Trigger]] = {
"_": SingleEntityEventTrigger,
"event_started": EventStartedTrigger,
"event_ended": EventEndedTrigger,
"_": EventTrigger,
}

View File

@@ -1,27 +0,0 @@
.trigger_common: &trigger_common
target:
entity:
domain: calendar
fields:
offset:
required: true
default:
days: 0
hours: 0
minutes: 0
seconds: 0
selector:
duration:
enable_day: true
offset_type:
required: true
default: before
selector:
select:
translation_key: trigger_offset_type
options:
- before
- after
event_started: *trigger_common
event_ended: *trigger_common

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from webexpythonsdk import ApiError, WebexAPI, exceptions
@@ -52,7 +51,7 @@ class CiscoWebexNotificationService(BaseNotificationService):
self.room = room
self.client = client
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
title = ""

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from http import HTTPStatus
import json
import logging
from typing import Any
import requests
import voluptuous as vol
@@ -82,7 +81,7 @@ class ClicksendNotificationService(BaseNotificationService):
self.language = config[CONF_LANGUAGE]
self.voice = config[CONF_VOICE]
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a voice call to a user."""
data = {
"messages": [

View File

@@ -49,11 +49,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Concord232 alarm control panel platform."""
name: str = config[CONF_NAME]
code: str | None = config.get(CONF_CODE)
mode: str = config[CONF_MODE]
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
name = config[CONF_NAME]
code = config.get(CONF_CODE)
mode = config[CONF_MODE]
host = config[CONF_HOST]
port = config[CONF_PORT]
url = f"http://{host}:{port}"
@@ -72,7 +72,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
)
def __init__(self, url: str, name: str, code: str | None, mode: str) -> None:
def __init__(self, url, name, code, mode):
"""Initialize the Concord232 alarm panel."""
self._attr_name = name
@@ -125,7 +125,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
return
self._alarm.arm("away")
def _validate_code(self, code: str | None, state: AlarmControlPanelState) -> bool:
def _validate_code(self, code, state):
"""Validate given code."""
if self._code is None:
return True

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import datetime
import logging
from typing import Any
from concord232 import client as concord232_client
import requests
@@ -30,7 +29,8 @@ CONF_ZONE_TYPES = "zone_types"
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "Alarm"
DEFAULT_PORT = 5007
DEFAULT_PORT = "5007"
DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=10)
@@ -56,10 +56,10 @@ def setup_platform(
) -> None:
"""Set up the Concord232 binary sensor platform."""
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
exclude: list[int] = config[CONF_EXCLUDE_ZONES]
zone_types: dict[int, BinarySensorDeviceClass] = config[CONF_ZONE_TYPES]
host = config[CONF_HOST]
port = config[CONF_PORT]
exclude = config[CONF_EXCLUDE_ZONES]
zone_types = config[CONF_ZONE_TYPES]
sensors = []
try:
@@ -84,6 +84,7 @@ def setup_platform(
if zone["number"] not in exclude:
sensors.append(
Concord232ZoneSensor(
hass,
client,
zone,
zone_types.get(zone["number"], get_opening_type(zone)),
@@ -109,25 +110,26 @@ def get_opening_type(zone):
class Concord232ZoneSensor(BinarySensorEntity):
"""Representation of a Concord232 zone as a sensor."""
def __init__(
self,
client: concord232_client.Client,
zone: dict[str, Any],
zone_type: BinarySensorDeviceClass,
) -> None:
def __init__(self, hass, client, zone, zone_type):
"""Initialize the Concord232 binary sensor."""
self._hass = hass
self._client = client
self._zone = zone
self._number = zone["number"]
self._attr_device_class = zone_type
self._zone_type = zone_type
@property
def name(self) -> str:
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@property
def name(self):
"""Return the name of the binary sensor."""
return self._zone["name"]
@property
def is_on(self) -> bool:
def is_on(self):
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
return bool(self._zone["state"] != "Normal")
@@ -143,5 +145,5 @@ class Concord232ZoneSensor(BinarySensorEntity):
if hasattr(self._client, "zones"):
self._zone = next(
x for x in self._client.zones if x["number"] == self._number
(x for x in self._client.zones if x["number"] == self._number), None
)

View File

@@ -11,11 +11,13 @@ from homeassistant import config_entries
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
from homeassistant.helpers.json import json_dumps
_LOGGER = logging.getLogger(__name__)
@@ -349,12 +351,26 @@ def websocket_get_automatic_entity_ids(
if not (entry := registry.entities.get(entity_id)):
automatic_entity_ids[entity_id] = None
continue
new_entity_id = registry.async_regenerate_entity_id(
entry,
try:
suggested = async_get_entity_suggested_object_id(hass, entity_id)
except HomeAssistantError as err:
# This is raised if the entity has no object.
_LOGGER.debug(
"Unable to get suggested object ID for %s, entity ID: %s (%s)",
entry.entity_id,
entity_id,
err,
)
automatic_entity_ids[entity_id] = None
continue
suggested_entity_id = registry.async_generate_entity_id(
entry.domain,
suggested or f"{entry.platform}_{entry.unique_id}",
current_entity_id=entity_id,
reserved_entity_ids=reserved_entity_ids,
)
automatic_entity_ids[entity_id] = new_entity_id
reserved_entity_ids.add(new_entity_id)
automatic_entity_ids[entity_id] = suggested_entity_id
reserved_entity_ids.add(suggested_entity_id)
connection.send_message(
websocket_api.result_message(msg["id"], automatic_entity_ids)

View File

@@ -7,5 +7,6 @@
"integration_type": "service",
"iot_class": "local_push",
"loggers": ["datadog"],
"quality_scale": "legacy",
"requirements": ["datadog==0.52.0"]
}

View File

@@ -1,7 +1,6 @@
"""Support for Digital Ocean."""
from __future__ import annotations
from datetime import timedelta
import logging
import digitalocean
@@ -13,12 +12,27 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle
from .const import DATA_DIGITAL_OCEAN, DOMAIN, MIN_TIME_BETWEEN_UPDATES
_LOGGER = logging.getLogger(__name__)
ATTR_CREATED_AT = "created_at"
ATTR_DROPLET_ID = "droplet_id"
ATTR_DROPLET_NAME = "droplet_name"
ATTR_FEATURES = "features"
ATTR_IPV4_ADDRESS = "ipv4_address"
ATTR_IPV6_ADDRESS = "ipv6_address"
ATTR_MEMORY = "memory"
ATTR_REGION = "region"
ATTR_VCPUS = "vcpus"
ATTRIBUTION = "Data provided by Digital Ocean"
CONF_DROPLETS = "droplets"
DATA_DIGITAL_OCEAN = "data_do"
DIGITAL_OCEAN_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR]
DOMAIN = "digital_ocean"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
@@ -17,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
from . import (
ATTR_CREATED_AT,
ATTR_DROPLET_ID,
ATTR_DROPLET_NAME,
@@ -66,7 +65,6 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
"""Representation of a Digital Ocean droplet sensor."""
_attr_attribution = ATTRIBUTION
_attr_device_class = BinarySensorDeviceClass.MOVING
def __init__(self, do, droplet_id):
"""Initialize a new Digital Ocean sensor."""
@@ -81,12 +79,17 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
return self.data.name
@property
def is_on(self) -> bool:
def is_on(self):
"""Return true if the binary sensor is on."""
return self.data.status == "active"
@property
def extra_state_attributes(self) -> dict[str, Any]:
def device_class(self):
"""Return the class of this sensor."""
return BinarySensorDeviceClass.MOVING
@property
def extra_state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_CREATED_AT: self.data.created_at,

View File

@@ -1,30 +0,0 @@
"""Support for Digital Ocean."""
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import DigitalOcean
ATTR_CREATED_AT = "created_at"
ATTR_DROPLET_ID = "droplet_id"
ATTR_DROPLET_NAME = "droplet_name"
ATTR_FEATURES = "features"
ATTR_IPV4_ADDRESS = "ipv4_address"
ATTR_IPV6_ADDRESS = "ipv6_address"
ATTR_MEMORY = "memory"
ATTR_REGION = "region"
ATTR_VCPUS = "vcpus"
ATTRIBUTION = "Data provided by Digital Ocean"
CONF_DROPLETS = "droplets"
DOMAIN = "digital_ocean"
DATA_DIGITAL_OCEAN: HassKey[DigitalOcean] = HassKey(DOMAIN)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
from . import (
ATTR_CREATED_AT,
ATTR_DROPLET_ID,
ATTR_DROPLET_NAME,
@@ -80,12 +80,12 @@ class DigitalOceanSwitch(SwitchEntity):
return self.data.name
@property
def is_on(self) -> bool:
def is_on(self):
"""Return true if switch is on."""
return self.data.status == "active"
@property
def extra_state_attributes(self) -> dict[str, Any]:
def extra_state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_CREATED_AT: self.data.created_at,

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.core import HomeAssistant
@@ -30,7 +29,7 @@ class DovadoSMSNotificationService(BaseNotificationService):
"""Initialize the service."""
self._client = client
def send_message(self, message: str, **kwargs: Any) -> None:
def send_message(self, message, **kwargs):
"""Send SMS to the specified target phone number."""
if not (target := kwargs.get(ATTR_TARGET)):
_LOGGER.error("One target is required")

View File

@@ -1,7 +1,6 @@
"""Support for Ebusd daemon for communication with eBUS heating systems."""
import logging
from typing import Any
import ebusdpy
import voluptuous as vol
@@ -18,7 +17,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, EBUSD_DATA, SENSOR_TYPES
from .const import DOMAIN, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
@@ -29,9 +28,9 @@ CACHE_TTL = 900
SERVICE_EBUSD_WRITE = "ebusd_write"
def verify_ebusd_config(config: ConfigType) -> ConfigType:
def verify_ebusd_config(config):
"""Verify eBusd config."""
circuit: str = config[CONF_CIRCUIT]
circuit = config[CONF_CIRCUIT]
for condition in config[CONF_MONITORED_CONDITIONS]:
if condition not in SENSOR_TYPES[circuit]:
raise vol.Invalid(f"Condition '{condition}' not in '{circuit}'.")
@@ -60,17 +59,17 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the eBusd component."""
_LOGGER.debug("Integration setup started")
conf: ConfigType = config[DOMAIN]
name: str = conf[CONF_NAME]
circuit: str = conf[CONF_CIRCUIT]
monitored_conditions: list[str] = conf[CONF_MONITORED_CONDITIONS]
server_address: tuple[str, int] = (conf[CONF_HOST], conf[CONF_PORT])
conf = config[DOMAIN]
name = conf[CONF_NAME]
circuit = conf[CONF_CIRCUIT]
monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS)
server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT))
try:
ebusdpy.init(server_address)
except (TimeoutError, OSError):
return False
hass.data[EBUSD_DATA] = EbusdData(server_address, circuit)
hass.data[DOMAIN] = EbusdData(server_address, circuit)
sensor_config = {
CONF_MONITORED_CONDITIONS: monitored_conditions,
"client_name": name,
@@ -78,7 +77,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
}
load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config)
hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[EBUSD_DATA].write)
hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write)
_LOGGER.debug("Ebusd integration setup completed")
return True
@@ -87,13 +86,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
class EbusdData:
"""Get the latest data from Ebusd."""
def __init__(self, address: tuple[str, int], circuit: str) -> None:
def __init__(self, address, circuit):
"""Initialize the data object."""
self._circuit = circuit
self._address = address
self.value: dict[str, Any] = {}
self.value = {}
def update(self, name: str, stype: int) -> None:
def update(self, name, stype):
"""Call the Ebusd API to update the data."""
try:
_LOGGER.debug("Opening socket to ebusd %s", name)

View File

@@ -1,9 +1,5 @@
"""Constants for ebus component."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
PERCENTAGE,
@@ -12,283 +8,277 @@ from homeassistant.const import (
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import EbusdData
DOMAIN = "ebusd"
EBUSD_DATA: HassKey[EbusdData] = HassKey(DOMAIN)
# SensorTypes from ebusdpy module :
# 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status'
type SensorSpecs = tuple[str, str | None, str | None, int, SensorDeviceClass | None]
SENSOR_TYPES: dict[str, dict[str, SensorSpecs]] = {
SENSOR_TYPES = {
"700": {
"ActualFlowTemperatureDesired": (
"ActualFlowTemperatureDesired": [
"Hc1ActualFlowTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"MaxFlowTemperatureDesired": (
],
"MaxFlowTemperatureDesired": [
"Hc1MaxFlowTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"MinFlowTemperatureDesired": (
],
"MinFlowTemperatureDesired": [
"Hc1MinFlowTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"PumpStatus": ("Hc1PumpStatus", None, "mdi:toggle-switch", 2, None),
"HCSummerTemperatureLimit": (
],
"PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2, None],
"HCSummerTemperatureLimit": [
"Hc1SummerTempLimit",
UnitOfTemperature.CELSIUS,
"mdi:weather-sunny",
0,
SensorDeviceClass.TEMPERATURE,
),
"HolidayTemperature": (
],
"HolidayTemperature": [
"HolidayTemp",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"HWTemperatureDesired": (
],
"HWTemperatureDesired": [
"HwcTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"HWActualTemperature": (
],
"HWActualTemperature": [
"HwcStorageTemp",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"HWTimerMonday": ("hwcTimer.Monday", None, "mdi:timer-outline", 1, None),
"HWTimerTuesday": ("hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None),
"HWTimerWednesday": ("hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None),
"HWTimerThursday": ("hwcTimer.Thursday", None, "mdi:timer-outline", 1, None),
"HWTimerFriday": ("hwcTimer.Friday", None, "mdi:timer-outline", 1, None),
"HWTimerSaturday": ("hwcTimer.Saturday", None, "mdi:timer-outline", 1, None),
"HWTimerSunday": ("hwcTimer.Sunday", None, "mdi:timer-outline", 1, None),
"HWOperativeMode": ("HwcOpMode", None, "mdi:math-compass", 3, None),
"WaterPressure": (
],
"HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1, None],
"HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None],
"HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None],
"HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1, None],
"HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1, None],
"HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1, None],
"HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1, None],
"HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3, None],
"WaterPressure": [
"WaterPressure",
UnitOfPressure.BAR,
"mdi:water-pump",
0,
SensorDeviceClass.PRESSURE,
),
"Zone1RoomZoneMapping": ("z1RoomZoneMapping", None, "mdi:label", 0, None),
"Zone1NightTemperature": (
],
"Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0, None],
"Zone1NightTemperature": [
"z1NightTemp",
UnitOfTemperature.CELSIUS,
"mdi:weather-night",
0,
SensorDeviceClass.TEMPERATURE,
),
"Zone1DayTemperature": (
],
"Zone1DayTemperature": [
"z1DayTemp",
UnitOfTemperature.CELSIUS,
"mdi:weather-sunny",
0,
SensorDeviceClass.TEMPERATURE,
),
"Zone1HolidayTemperature": (
],
"Zone1HolidayTemperature": [
"z1HolidayTemp",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"Zone1RoomTemperature": (
],
"Zone1RoomTemperature": [
"z1RoomTemp",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"Zone1ActualRoomTemperatureDesired": (
],
"Zone1ActualRoomTemperatureDesired": [
"z1ActualRoomTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"Zone1TimerMonday": ("z1Timer.Monday", None, "mdi:timer-outline", 1, None),
"Zone1TimerTuesday": ("z1Timer.Tuesday", None, "mdi:timer-outline", 1, None),
"Zone1TimerWednesday": (
],
"Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1, None],
"Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1, None],
"Zone1TimerWednesday": [
"z1Timer.Wednesday",
None,
"mdi:timer-outline",
1,
None,
),
"Zone1TimerThursday": ("z1Timer.Thursday", None, "mdi:timer-outline", 1, None),
"Zone1TimerFriday": ("z1Timer.Friday", None, "mdi:timer-outline", 1, None),
"Zone1TimerSaturday": ("z1Timer.Saturday", None, "mdi:timer-outline", 1, None),
"Zone1TimerSunday": ("z1Timer.Sunday", None, "mdi:timer-outline", 1, None),
"Zone1OperativeMode": ("z1OpMode", None, "mdi:math-compass", 3, None),
"ContinuosHeating": (
],
"Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1, None],
"Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1, None],
"Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1, None],
"Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1, None],
"Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3, None],
"ContinuosHeating": [
"ContinuosHeating",
UnitOfTemperature.CELSIUS,
"mdi:weather-snowy",
0,
SensorDeviceClass.TEMPERATURE,
),
"PowerEnergyConsumptionLastMonth": (
],
"PowerEnergyConsumptionLastMonth": [
"PrEnergySumHcLastMonth",
UnitOfEnergy.KILO_WATT_HOUR,
"mdi:flash",
0,
SensorDeviceClass.ENERGY,
),
"PowerEnergyConsumptionThisMonth": (
],
"PowerEnergyConsumptionThisMonth": [
"PrEnergySumHcThisMonth",
UnitOfEnergy.KILO_WATT_HOUR,
"mdi:flash",
0,
SensorDeviceClass.ENERGY,
),
],
},
"ehp": {
"HWTemperature": (
"HWTemperature": [
"HwcTemp",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
"OutsideTemp": (
],
"OutsideTemp": [
"OutsideTemp",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
],
},
"bai": {
"HotWaterTemperature": (
"HotWaterTemperature": [
"HwcTemp",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
"StorageTemperature": (
],
"StorageTemperature": [
"StorageTemp",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
"DesiredStorageTemperature": (
],
"DesiredStorageTemperature": [
"StorageTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"OutdoorsTemperature": (
],
"OutdoorsTemperature": [
"OutdoorstempSensor",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
"WaterPressure": (
],
"WaterPressure": [
"WaterPressure",
UnitOfPressure.BAR,
"mdi:pipe",
4,
SensorDeviceClass.PRESSURE,
),
"AverageIgnitionTime": (
],
"AverageIgnitionTime": [
"averageIgnitiontime",
UnitOfTime.SECONDS,
"mdi:av-timer",
0,
SensorDeviceClass.DURATION,
),
"MaximumIgnitionTime": (
],
"MaximumIgnitionTime": [
"maxIgnitiontime",
UnitOfTime.SECONDS,
"mdi:av-timer",
0,
SensorDeviceClass.DURATION,
),
"MinimumIgnitionTime": (
],
"MinimumIgnitionTime": [
"minIgnitiontime",
UnitOfTime.SECONDS,
"mdi:av-timer",
0,
SensorDeviceClass.DURATION,
),
"ReturnTemperature": (
],
"ReturnTemperature": [
"ReturnTemp",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
"CentralHeatingPump": ("WP", None, "mdi:toggle-switch", 2, None),
"HeatingSwitch": ("HeatingSwitch", None, "mdi:toggle-switch", 2, None),
"DesiredFlowTemperature": (
],
"CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2, None],
"HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2, None],
"DesiredFlowTemperature": [
"FlowTempDesired",
UnitOfTemperature.CELSIUS,
None,
0,
SensorDeviceClass.TEMPERATURE,
),
"FlowTemperature": (
],
"FlowTemperature": [
"FlowTemp",
UnitOfTemperature.CELSIUS,
None,
4,
SensorDeviceClass.TEMPERATURE,
),
"Flame": ("Flame", None, "mdi:toggle-switch", 2, None),
"PowerEnergyConsumptionHeatingCircuit": (
],
"Flame": ["Flame", None, "mdi:toggle-switch", 2, None],
"PowerEnergyConsumptionHeatingCircuit": [
"PrEnergySumHc1",
UnitOfEnergy.KILO_WATT_HOUR,
"mdi:flash",
0,
SensorDeviceClass.ENERGY,
),
"PowerEnergyConsumptionHotWaterCircuit": (
],
"PowerEnergyConsumptionHotWaterCircuit": [
"PrEnergySumHwc1",
UnitOfEnergy.KILO_WATT_HOUR,
"mdi:flash",
0,
SensorDeviceClass.ENERGY,
),
"RoomThermostat": ("DCRoomthermostat", None, "mdi:toggle-switch", 2, None),
"HeatingPartLoad": (
],
"RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2, None],
"HeatingPartLoad": [
"PartloadHcKW",
UnitOfEnergy.KILO_WATT_HOUR,
"mdi:flash",
0,
SensorDeviceClass.ENERGY,
),
"StateNumber": ("StateNumber", None, "mdi:fire", 3, None),
"ModulationPercentage": (
],
"StateNumber": ["StateNumber", None, "mdi:fire", 3, None],
"ModulationPercentage": [
"ModulationTempDesired",
PERCENTAGE,
"mdi:percent",
0,
None,
),
],
},
}

View File

@@ -4,16 +4,14 @@ from __future__ import annotations
import datetime
import logging
from typing import Any, cast
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle, dt as dt_util
from . import EbusdData
from .const import EBUSD_DATA, SensorSpecs
from .const import DOMAIN
TIME_FRAME1_BEGIN = "time_frame1_begin"
TIME_FRAME1_END = "time_frame1_end"
@@ -35,9 +33,9 @@ def setup_platform(
"""Set up the Ebus sensor."""
if not discovery_info:
return
ebusd_api = hass.data[EBUSD_DATA]
monitored_conditions: list[str] = discovery_info["monitored_conditions"]
name: str = discovery_info["client_name"]
ebusd_api = hass.data[DOMAIN]
monitored_conditions = discovery_info["monitored_conditions"]
name = discovery_info["client_name"]
add_entities(
(
@@ -51,8 +49,9 @@ def setup_platform(
class EbusdSensor(SensorEntity):
"""Ebusd component sensor methods definition."""
def __init__(self, data: EbusdData, sensor: SensorSpecs, name: str) -> None:
def __init__(self, data, sensor, name):
"""Initialize the sensor."""
self._state = None
self._client_name = name
(
self._name,
@@ -64,15 +63,20 @@ class EbusdSensor(SensorEntity):
self.data = data
@property
def name(self) -> str:
def name(self):
"""Return the name of the sensor."""
return f"{self._client_name} {self._name}"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
def native_value(self):
"""Return the state of the sensor."""
return self._state
@property
def extra_state_attributes(self):
"""Return the device state attributes."""
if self._type == 1 and (native_value := self.native_value) is not None:
schedule: dict[str, str | None] = {
if self._type == 1 and self._state is not None:
schedule = {
TIME_FRAME1_BEGIN: None,
TIME_FRAME1_END: None,
TIME_FRAME2_BEGIN: None,
@@ -80,7 +84,7 @@ class EbusdSensor(SensorEntity):
TIME_FRAME3_BEGIN: None,
TIME_FRAME3_END: None,
}
time_frame = cast(str, native_value).split(";")
time_frame = self._state.split(";")
for index, item in enumerate(sorted(schedule.items())):
if index < len(time_frame):
parsed = datetime.datetime.strptime(time_frame[index], "%H:%M")
@@ -92,17 +96,17 @@ class EbusdSensor(SensorEntity):
return None
@property
def device_class(self) -> SensorDeviceClass | None:
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class
@property
def icon(self) -> str | None:
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon
@property
def native_unit_of_measurement(self) -> str | None:
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@@ -114,6 +118,6 @@ class EbusdSensor(SensorEntity):
if self._name not in self.data.value:
return
self._attr_native_value = self.data.value[self._name]
self._state = self.data.value[self._name]
except RuntimeError:
_LOGGER.debug("EbusdData.update exception")

View File

@@ -18,7 +18,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = "GATE-01"
DOMAIN = "egardia"
EGARDIA_DEVICE: HassKey[egardiadevice.EgardiaDevice] = HassKey(DOMAIN)
EGARDIA_DEVICE = "egardiadevice"
EGARDIA_NAME = "egardianame"
EGARDIA_REPORT_SERVER_CODES = "egardia_rs_codes"
EGARDIA_REPORT_SERVER_ENABLED = "egardia_rs_enabled"

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
from pythonegardia.egardiadevice import EgardiaDevice
import requests
from homeassistant.components.alarm_control_panel import (
@@ -12,7 +11,6 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -49,10 +47,10 @@ def setup_platform(
if discovery_info is None:
return
device = EgardiaAlarm(
discovery_info[CONF_NAME],
discovery_info["name"],
hass.data[EGARDIA_DEVICE],
discovery_info[CONF_REPORT_SERVER_ENABLED],
discovery_info[CONF_REPORT_SERVER_CODES],
discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_PORT],
)
@@ -69,13 +67,8 @@ class EgardiaAlarm(AlarmControlPanelEntity):
)
def __init__(
self,
name: str,
egardiasystem: EgardiaDevice,
rs_enabled: bool,
rs_codes: dict[str, list[str]],
rs_port: int,
) -> None:
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
):
"""Initialize the Egardia alarm."""
self._attr_name = name
self._egardiasystem = egardiasystem
@@ -92,7 +85,9 @@ class EgardiaAlarm(AlarmControlPanelEntity):
@property
def should_poll(self) -> bool:
"""Poll if no report server is enabled."""
return not self._rs_enabled
if not self._rs_enabled:
return True
return False
def handle_status_event(self, event):
"""Handle the Egardia system status event."""

View File

@@ -2,12 +2,11 @@
from __future__ import annotations
from pythonegardia.egardiadevice import EgardiaDevice
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -52,20 +51,30 @@ async def async_setup_platform(
class EgardiaBinarySensor(BinarySensorEntity):
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
def __init__(
self,
sensor_id: str,
name: str,
egardia_system: EgardiaDevice,
device_class: BinarySensorDeviceClass | None,
) -> None:
def __init__(self, sensor_id, name, egardia_system, device_class):
"""Initialize the sensor device."""
self._id = sensor_id
self._attr_name = name
self._attr_device_class = device_class
self._name = name
self._state = None
self._device_class = device_class
self._egardia_system = egardia_system
def update(self) -> None:
"""Update the status."""
egardia_input = self._egardia_system.getsensorstate(self._id)
self._attr_is_on = bool(egardia_input)
self._state = STATE_ON if egardia_input else STATE_OFF
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def is_on(self):
"""Whether the device is switched on."""
return self._state == STATE_ON
@property
def device_class(self):
"""Return the device class."""
return self._device_class

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.3"],
"requirements": ["pyenphase==2.4.2"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -18,13 +18,12 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
DOMAIN = "envisalink"
DATA_EVL: HassKey[EnvisalinkAlarmPanel] = HassKey(DOMAIN)
DATA_EVL = "envisalink"
CONF_EVL_KEEPALIVE = "keepalive_interval"
CONF_EVL_PORT = "port"

View File

@@ -3,9 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
@@ -24,7 +22,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_PANIC,
CONF_PARTITIONNAME,
CONF_PARTITIONS,
DATA_EVL,
DOMAIN,
PARTITION_SCHEMA,
@@ -54,14 +51,15 @@ async def async_setup_platform(
"""Perform the setup for Envisalink alarm panels."""
if not discovery_info:
return
configured_partitions: dict[int, dict[str, Any]] = discovery_info[CONF_PARTITIONS]
code: str | None = discovery_info[CONF_CODE]
panic_type: str = discovery_info[CONF_PANIC]
configured_partitions = discovery_info["partitions"]
code = discovery_info[CONF_CODE]
panic_type = discovery_info[CONF_PANIC]
entities = []
for part_num, part_config in configured_partitions.items():
entity_config_data = PARTITION_SCHEMA(part_config)
for part_num in configured_partitions:
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
entity = EnvisalinkAlarm(
hass,
part_num,
entity_config_data[CONF_PARTITIONNAME],
code,
@@ -105,14 +103,8 @@ class EnvisalinkAlarm(EnvisalinkEntity, AlarmControlPanelEntity):
)
def __init__(
self,
partition_number: int,
alarm_name: str,
code: str | None,
panic_type: str,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
self, hass, partition_number, alarm_name, code, panic_type, info, controller
):
"""Initialize the alarm panel."""
self._partition_number = partition_number
self._panic_type = panic_type

View File

@@ -4,14 +4,8 @@ from __future__ import annotations
import datetime
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import ATTR_LAST_TRIP_TIME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -19,14 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from . import (
CONF_ZONENAME,
CONF_ZONES,
CONF_ZONETYPE,
DATA_EVL,
SIGNAL_ZONE_UPDATE,
ZONE_SCHEMA,
)
from . import CONF_ZONENAME, CONF_ZONETYPE, DATA_EVL, SIGNAL_ZONE_UPDATE, ZONE_SCHEMA
from .entity import EnvisalinkEntity
_LOGGER = logging.getLogger(__name__)
@@ -41,12 +28,13 @@ async def async_setup_platform(
"""Set up the Envisalink binary sensor entities."""
if not discovery_info:
return
configured_zones: dict[int, dict[str, Any]] = discovery_info[CONF_ZONES]
configured_zones = discovery_info["zones"]
entities = []
for zone_num, zone_data in configured_zones.items():
entity_config_data = ZONE_SCHEMA(zone_data)
for zone_num in configured_zones:
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
entity = EnvisalinkBinarySensor(
hass,
zone_num,
entity_config_data[CONF_ZONENAME],
entity_config_data[CONF_ZONETYPE],
@@ -61,16 +49,9 @@ async def async_setup_platform(
class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
"""Representation of an Envisalink binary sensor."""
def __init__(
self,
zone_number: int,
zone_name: str,
zone_type: BinarySensorDeviceClass,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
"""Initialize the binary_sensor."""
self._attr_device_class = zone_type
self._zone_type = zone_type
self._zone_number = zone_number
_LOGGER.debug("Setting up zone: %s", zone_name)
@@ -85,9 +66,9 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
def extra_state_attributes(self):
"""Return the state attributes."""
attr: dict[str, Any] = {}
attr = {}
# The Envisalink library returns a "last_fault" value that's the
# number of seconds since the last fault, up to a maximum of 327680
@@ -120,6 +101,11 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
"""Return true if sensor is on."""
return self._info["status"]["open"]
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@callback
def async_update_callback(self, zone):
"""Update the zone's state, if needed."""

View File

@@ -1,9 +1,5 @@
"""Support for Envisalink devices."""
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.helpers.entity import Entity
@@ -12,10 +8,13 @@ class EnvisalinkEntity(Entity):
_attr_should_poll = False
def __init__(
self, name: str, info: dict[str, Any], controller: EnvisalinkAlarmPanel
) -> None:
def __init__(self, name, info, controller):
"""Initialize the device."""
self._controller = controller
self._info = info
self._attr_name = name
self._name = name
@property
def name(self):
"""Return the name of the device."""
return self._name

View File

@@ -3,9 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant, callback
@@ -15,7 +12,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_PARTITIONNAME,
CONF_PARTITIONS,
DATA_EVL,
PARTITION_SCHEMA,
SIGNAL_KEYPAD_UPDATE,
@@ -35,12 +31,13 @@ async def async_setup_platform(
"""Perform the setup for Envisalink sensor entities."""
if not discovery_info:
return
configured_partitions: dict[int, dict[str, Any]] = discovery_info[CONF_PARTITIONS]
configured_partitions = discovery_info["partitions"]
entities = []
for part_num, part_config in configured_partitions.items():
entity_config_data = PARTITION_SCHEMA(part_config)
for part_num in configured_partitions:
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
entity = EnvisalinkSensor(
hass,
entity_config_data[CONF_PARTITIONNAME],
part_num,
hass.data[DATA_EVL].alarm_state["partition"][part_num],
@@ -55,16 +52,9 @@ async def async_setup_platform(
class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
"""Representation of an Envisalink keypad."""
_attr_icon = "mdi:alarm"
def __init__(
self,
partition_name: str,
partition_number: int,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
def __init__(self, hass, partition_name, partition_number, info, controller):
"""Initialize the sensor."""
self._icon = "mdi:alarm"
self._partition_number = partition_number
_LOGGER.debug("Setting up sensor for partition: %s", partition_name)
@@ -83,6 +73,11 @@ class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
)
)
@property
def icon(self):
"""Return the icon if any."""
return self._icon
@property
def native_value(self):
"""Return the overall state."""

View File

@@ -5,21 +5,13 @@ from __future__ import annotations
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_ZONENAME,
CONF_ZONES,
DATA_EVL,
SIGNAL_ZONE_BYPASS_UPDATE,
ZONE_SCHEMA,
)
from . import CONF_ZONENAME, DATA_EVL, SIGNAL_ZONE_BYPASS_UPDATE, ZONE_SCHEMA
from .entity import EnvisalinkEntity
_LOGGER = logging.getLogger(__name__)
@@ -34,15 +26,16 @@ async def async_setup_platform(
"""Set up the Envisalink switch entities."""
if not discovery_info:
return
configured_zones: dict[int, dict[str, Any]] = discovery_info[CONF_ZONES]
configured_zones = discovery_info["zones"]
entities = []
for zone_num, zone_data in configured_zones.items():
entity_config_data = ZONE_SCHEMA(zone_data)
for zone_num in configured_zones:
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_name = f"{entity_config_data[CONF_ZONENAME]}_bypass"
_LOGGER.debug("Setting up zone_bypass switch: %s", zone_name)
entity = EnvisalinkSwitch(
hass,
zone_num,
zone_name,
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
@@ -56,13 +49,7 @@ async def async_setup_platform(
class EnvisalinkSwitch(EnvisalinkEntity, SwitchEntity):
"""Representation of an Envisalink switch."""
def __init__(
self,
zone_number: int,
zone_name: str,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
def __init__(self, hass, zone_number, zone_name, info, controller):
"""Initialize the switch."""
self._zone_number = zone_number

View File

@@ -7,7 +7,7 @@ from enum import StrEnum
from typing import Final
DOMAIN: Final = "essent"
UPDATE_INTERVAL: Final = timedelta(hours=1)
UPDATE_INTERVAL: Final = timedelta(hours=12)
ATTRIBUTION: Final = "Data provided by Essent"

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from http import HTTPStatus
import json
import logging
from typing import Any
import requests
import voluptuous as vol
@@ -47,7 +46,7 @@ class FacebookNotificationService(BaseNotificationService):
"""Initialize the service."""
self.page_access_token = access_token
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send some message."""
payload = {"access_token": self.page_access_token}
targets = kwargs.get(ATTR_TARGET)

View File

@@ -1,17 +0,0 @@
"""Provides conditions for fans."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the fan conditions."""
return CONDITIONS

View File

@@ -1,17 +0,0 @@
.condition_common: &condition_common
target:
entity:
domain: fan
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_off: *condition_common
is_on: *condition_common

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_off": {
"condition": "mdi:fan-off"
},
"is_on": {
"condition": "mdi:fan"
}
},
"entity_component": {
"_": {
"default": "mdi:fan",

View File

@@ -1,32 +1,8 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted fans.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted fans to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_off": {
"description": "Tests if one or more fans are off.",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::condition_behavior_description%]",
"name": "[%key:component::fan::common::condition_behavior_name%]"
}
},
"name": "If a fan is off"
},
"is_on": {
"description": "Tests if one or more fans are on.",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::condition_behavior_description%]",
"name": "[%key:component::fan::common::condition_behavior_name%]"
}
},
"name": "If a fan is on"
}
},
"device_automation": {
"action_type": {
"toggle": "[%key:common::device_automation::action_type::toggle%]",
@@ -89,12 +65,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"direction": {
"options": {
"forward": "Forward",

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
@@ -98,30 +97,17 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
end_date = now
try:
(
accounts,
categories,
primary_currency,
budgets,
bills,
) = await asyncio.gather(
self.firefly.get_accounts(),
self.firefly.get_categories(),
self.firefly.get_currency_primary(),
self.firefly.get_budgets(start=start_date, end=end_date),
self.firefly.get_bills(),
)
category_details = await asyncio.gather(
*(
self.firefly.get_category(
category_id=int(category.id),
start=start_date,
end=end_date,
)
for category in categories
accounts = await self.firefly.get_accounts()
categories = await self.firefly.get_categories()
category_details = [
await self.firefly.get_category(
category_id=int(category.id), start=start_date, end=end_date
)
)
for category in categories
]
primary_currency = await self.firefly.get_currency_primary()
budgets = await self.firefly.get_budgets(start=start_date, end=end_date)
bills = await self.firefly.get_bills()
except FireflyAuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyfirefly==0.1.11"]
"requirements": ["pyfirefly==0.1.10"]
}

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio
from http import HTTPStatus
import logging
from typing import Any
import voluptuous as vol
@@ -48,7 +47,7 @@ class FlockNotificationService(BaseNotificationService):
self._url = url
self._session = session
async def async_send_message(self, message: str, **kwargs: Any) -> None:
async def async_send_message(self, message, **kwargs):
"""Send the message to the user."""
payload = {"text": message}

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from http import HTTPStatus
import logging
from typing import Any
from freesms import FreeClient
import voluptuous as vol
@@ -41,7 +40,7 @@ class FreeSMSNotificationService(BaseNotificationService):
"""Initialize the service."""
self.free_client = FreeClient(username, access_token)
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a message to the Free Mobile user cell."""
resp = self.free_client.send_sms(message)

View File

@@ -66,7 +66,6 @@ from .const import (
CONF_COLD_TOLERANCE,
CONF_HEATER,
CONF_HOT_TOLERANCE,
CONF_KEEP_ALIVE,
CONF_MAX_TEMP,
CONF_MIN_DUR,
CONF_MIN_TEMP,
@@ -82,6 +81,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Generic Thermostat"
CONF_INITIAL_HVAC_MODE = "initial_hvac_mode"
CONF_KEEP_ALIVE = "keep_alive"
CONF_PRECISION = "precision"
CONF_TARGET_TEMP = "target_temp"
CONF_TEMP_STEP = "target_temp_step"

View File

@@ -21,7 +21,6 @@ from .const import (
CONF_COLD_TOLERANCE,
CONF_HEATER,
CONF_HOT_TOLERANCE,
CONF_KEEP_ALIVE,
CONF_MAX_TEMP,
CONF_MIN_DUR,
CONF_MIN_TEMP,
@@ -60,9 +59,6 @@ OPTIONS_SCHEMA = {
vol.Optional(CONF_MIN_DUR): selector.DurationSelector(
selector.DurationSelectorConfig(allow_negative=False)
),
vol.Optional(CONF_KEEP_ALIVE): selector.DurationSelector(
selector.DurationSelectorConfig(allow_negative=False)
),
vol.Optional(CONF_MIN_TEMP): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE, step=0.1

View File

@@ -33,5 +33,4 @@ CONF_PRESETS = {
)
}
CONF_SENSOR = "target_sensor"
CONF_KEEP_ALIVE = "keep_alive"
DEFAULT_TOLERANCE = 0.3

View File

@@ -18,7 +18,6 @@
"cold_tolerance": "Cold tolerance",
"heater": "Actuator switch",
"hot_tolerance": "Hot tolerance",
"keep_alive": "Keep-alive interval",
"max_temp": "Maximum target temperature",
"min_cycle_duration": "Minimum cycle duration",
"min_temp": "Minimum target temperature",
@@ -30,7 +29,6 @@
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor goes below 24.5.",
"heater": "Switch entity used to cool or heat depending on A/C mode.",
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5.",
"keep_alive": "Trigger the heater periodically to keep devices from losing state. When set, min cycle duration is ignored.",
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
"target_sensor": "Temperature sensor that reflects the current temperature."
},
@@ -47,7 +45,6 @@
"cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data::cold_tolerance%]",
"heater": "[%key:component::generic_thermostat::config::step::user::data::heater%]",
"hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data::hot_tolerance%]",
"keep_alive": "[%key:component::generic_thermostat::config::step::user::data::keep_alive%]",
"max_temp": "[%key:component::generic_thermostat::config::step::user::data::max_temp%]",
"min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data::min_cycle_duration%]",
"min_temp": "[%key:component::generic_thermostat::config::step::user::data::min_temp%]",
@@ -58,7 +55,6 @@
"cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::cold_tolerance%]",
"heater": "[%key:component::generic_thermostat::config::step::user::data_description::heater%]",
"hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::hot_tolerance%]",
"keep_alive": "[%key:component::generic_thermostat::config::step::user::data_description::keep_alive%]",
"min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data_description::min_cycle_duration%]",
"target_sensor": "[%key:component::generic_thermostat::config::step::user::data_description::target_sensor%]"
}

View File

@@ -57,7 +57,7 @@ class GeniusSwitch(GeniusZone, SwitchEntity):
"""Representation of a Genius Hub switch."""
@property
def device_class(self) -> SwitchDeviceClass:
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return SwitchDeviceClass.OUTLET

View File

@@ -112,7 +112,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CO,
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
exists_fn=lambda x: "co" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.co.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -144,7 +143,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
translation_key="nitrogen_dioxide",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.no2.concentration.units,
exists_fn=lambda x: "no2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.no2.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -152,7 +150,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
translation_key="ozone",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.o3.concentration.units,
exists_fn=lambda x: "o3" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.o3.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -160,7 +157,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement_fn=lambda x: x.pollutants.pm10.concentration.units,
exists_fn=lambda x: "pm10" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.pm10.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -168,7 +164,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement_fn=lambda x: x.pollutants.pm25.concentration.units,
exists_fn=lambda x: "pm25" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.pm25.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -176,7 +171,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
translation_key="sulphur_dioxide",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.so2.concentration.units,
exists_fn=lambda x: "so2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.so2.concentration.value,
),
)

View File

@@ -346,6 +346,7 @@ class SensorGroup(GroupEntity, SensorEntity):
self._attr_name = name
if name == DEFAULT_NAME:
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
self._ignore_non_numeric = ignore_non_numeric
self.mode = all if ignore_non_numeric is False else any
@@ -373,7 +374,7 @@ class SensorGroup(GroupEntity, SensorEntity):
def async_update_group_state(self) -> None:
"""Query all members and determine the sensor group state."""
self.calculate_state_attributes(self._get_valid_entities())
states: list[str | None] = []
states: list[str] = []
valid_units = self._valid_units
valid_states: list[bool] = []
sensor_values: list[tuple[str, float, State]] = []
@@ -434,12 +435,9 @@ class SensorGroup(GroupEntity, SensorEntity):
state.attributes.get("unit_of_measurement"),
self.entity_id,
)
else:
states.append(None)
valid_states.append(False)
# Set group as unavailable if all members are unavailable or missing
self._attr_available = not all(s in (STATE_UNAVAILABLE, None) for s in states)
# Set group as unavailable if all members do not have numeric values
self._attr_available = any(numeric_state for numeric_state in valid_states)
valid_state = self.mode(
state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
@@ -448,7 +446,6 @@ class SensorGroup(GroupEntity, SensorEntity):
if not valid_state or not valid_state_numeric:
self._attr_native_value = None
self._extra_state_attribute = {}
return
# Calculate values

View File

@@ -8,7 +8,6 @@ from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
PLATFORMS = [
Platform.BUTTON,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -16,50 +16,6 @@
"default": "mdi:hdmi-port"
}
},
"sensor": {
"aud0": {
"default": "mdi:audio-input-rca"
},
"aud1": {
"default": "mdi:audio-input-rca"
},
"audout": {
"default": "mdi:television-speaker"
},
"earcrx": {
"default": "mdi:audio-video"
},
"edida0": {
"default": "mdi:format-list-text"
},
"edida1": {
"default": "mdi:format-list-text"
},
"edida2": {
"default": "mdi:format-list-text"
},
"rx0": {
"default": "mdi:video-input-hdmi"
},
"rx1": {
"default": "mdi:video-input-hdmi"
},
"sink0": {
"default": "mdi:television"
},
"sink1": {
"default": "mdi:television"
},
"sink2": {
"default": "mdi:audio-video"
},
"tx0": {
"default": "mdi:cable-data"
},
"tx1": {
"default": "mdi:cable-data"
}
},
"switch": {
"autosw": {
"default": "mdi:import"

View File

@@ -1,121 +0,0 @@
"""Sensor platform for HDFury Integration."""
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="RX0",
translation_key="rx0",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="RX1",
translation_key="rx1",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="TX0",
translation_key="tx0",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="TX1",
translation_key="tx1",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="AUD0",
translation_key="aud0",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="AUD1",
translation_key="aud1",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="AUDOUT",
translation_key="audout",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="EARCRX",
translation_key="earcrx",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="SINK0",
translation_key="sink0",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="SINK1",
translation_key="sink1",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="SINK2",
translation_key="sink2",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="EDIDA0",
translation_key="edida0",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="EDIDA1",
translation_key="edida1",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="EDIDA2",
translation_key="edida2",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HDFuryConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensors using the platform schema."""
coordinator = entry.runtime_data
async_add_entities(
HDFurySensor(coordinator, description)
for description in SENSORS
if description.key in coordinator.data.info
)
class HDFurySensor(HDFuryEntity, SensorEntity):
"""Base HDFury Sensor Class."""
entity_description: SensorEntityDescription
@property
def native_value(self) -> str:
"""Set Sensor Value."""
return self.coordinator.data.info[self.entity_description.key]

View File

@@ -57,50 +57,6 @@
}
}
},
"sensor": {
"aud0": {
"name": "Audio TX0"
},
"aud1": {
"name": "Audio TX1"
},
"audout": {
"name": "Audio output"
},
"earcrx": {
"name": "eARC/ARC status"
},
"edida0": {
"name": "EDID TXA0"
},
"edida1": {
"name": "EDID TXA1"
},
"edida2": {
"name": "EDID AUDA"
},
"rx0": {
"name": "Input RX0"
},
"rx1": {
"name": "Input RX1"
},
"sink0": {
"name": "EDID TX0"
},
"sink1": {
"name": "EDID TX1"
},
"sink2": {
"name": "EDID AUD"
},
"tx0": {
"name": "Output TX0"
},
"tx1": {
"name": "Output TX1"
}
},
"switch": {
"autosw": {
"name": "Auto switch inputs"

View File

@@ -191,11 +191,7 @@ class HikvisionBinarySensor(BinarySensorEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
via_device=(DOMAIN, self._data.device_id),
translation_key="nvr_channel",
translation_placeholders={
"device_name": self._data.device_name,
"channel_number": str(channel),
},
name=f"{self._data.device_name} Channel {channel}",
manufacturer="Hikvision",
model="NVR Channel",
)

View File

@@ -62,11 +62,7 @@ class HikvisionCamera(Camera):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
via_device=(DOMAIN, self._data.device_id),
translation_key="nvr_channel",
translation_placeholders={
"device_name": self._data.device_name,
"channel_number": str(channel),
},
name=f"{self._data.device_name} Channel {channel}",
manufacturer="Hikvision",
model="NVR Channel",
)

View File

@@ -29,11 +29,6 @@
}
}
},
"device": {
"nvr_channel": {
"name": "{device_name} channel {channel_number}"
}
},
"issues": {
"deprecated_yaml_import_issue": {
"description": "Configuring {integration_title} using YAML is deprecated and the import failed. Please remove the `{domain}` entry from your `configuration.yaml` file and set up the integration manually.",

View File

@@ -220,33 +220,31 @@ def get_accessory( # noqa: C901
a_type = "TemperatureSensor"
elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE:
a_type = "HumiditySensor"
elif device_class == SensorDeviceClass.PM10:
elif (
device_class == SensorDeviceClass.PM10
or SensorDeviceClass.PM10 in state.entity_id
):
a_type = "PM10Sensor"
elif device_class == SensorDeviceClass.PM25:
elif (
device_class == SensorDeviceClass.PM25
or SensorDeviceClass.PM25 in state.entity_id
):
a_type = "PM25Sensor"
elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE:
a_type = "NitrogenDioxideSensor"
elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS:
a_type = "VolatileOrganicCompoundsSensor"
elif device_class == SensorDeviceClass.GAS:
elif (
device_class == SensorDeviceClass.GAS
or SensorDeviceClass.GAS in state.entity_id
):
a_type = "AirQualitySensor"
elif device_class == SensorDeviceClass.CO:
a_type = "CarbonMonoxideSensor"
elif device_class == SensorDeviceClass.CO2:
elif device_class == SensorDeviceClass.CO2 or "co2" in state.entity_id:
a_type = "CarbonDioxideSensor"
elif device_class == SensorDeviceClass.ILLUMINANCE or unit == LIGHT_LUX:
a_type = "LightSensor"
# Fallbacks based on entity_id
elif SensorDeviceClass.PM10 in state.entity_id:
a_type = "PM10Sensor"
elif SensorDeviceClass.PM25 in state.entity_id:
a_type = "PM25Sensor"
elif SensorDeviceClass.GAS in state.entity_id:
a_type = "AirQualitySensor"
elif "co2" in state.entity_id:
a_type = "CarbonDioxideSensor"
else:
_LOGGER.debug(
"%s: Unsupported sensor type (device_class=%s) (unit=%s)",

View File

@@ -59,21 +59,21 @@ class HMBinarySensor(HMDevice, BinarySensorEntity):
"""Representation of a binary HomeMatic device."""
@property
def is_on(self) -> bool:
def is_on(self):
"""Return true if switch is on."""
if not self.available:
return False
return bool(self._hm_get_state())
@property
def device_class(self) -> BinarySensorDeviceClass | None:
def device_class(self):
"""Return the class of this sensor from DEVICE_CLASSES."""
# If state is MOTION (Only RemoteMotion working)
if self._state == "MOTION":
return BinarySensorDeviceClass.MOTION
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__)
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate the data dictionary (self._data) from metadata."""
# Add state to data struct
if self._state:
@@ -86,11 +86,11 @@ class HMBatterySensor(HMDevice, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.BATTERY
@property
def is_on(self) -> bool:
def is_on(self):
"""Return True if battery is low."""
return bool(self._hm_get_state())
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate the data dictionary (self._data) from metadata."""
# Add state to data struct
if self._state:

View File

@@ -178,7 +178,7 @@ class HMThermostat(HMDevice, ClimateEntity):
# Homematic
return self._data.get("CONTROL_MODE")
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata."""
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
self._data[self._state] = None

View File

@@ -78,7 +78,7 @@ class HMCover(HMDevice, CoverEntity):
"""Stop the device if in motion."""
self._hmdevice.stop(self._channel)
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate a data dictionary (self._data) from metadata."""
self._state = "LEVEL"
self._data.update({self._state: None})
@@ -138,7 +138,7 @@ class HMGarage(HMCover):
"""Return whether the cover is closed."""
return self._hmdevice.is_closed(self._hm_get_state())
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate a data dictionary (self._data) from metadata."""
self._state = "DOOR_STATE"
self._data.update({self._state: None})

View File

@@ -204,7 +204,7 @@ class HMDevice(Entity):
self._init_data_struct()
@abstractmethod
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate a data dictionary from the HomeMatic device metadata."""

View File

@@ -51,7 +51,7 @@ class HMLight(HMDevice, LightEntity):
_attr_max_color_temp_kelvin = 6500 # 153 Mireds
@property
def brightness(self) -> int | None:
def brightness(self):
"""Return the brightness of this light between 0..255."""
# Is dimmer?
if self._state == "LEVEL":
@@ -59,7 +59,7 @@ class HMLight(HMDevice, LightEntity):
return None
@property
def is_on(self) -> bool:
def is_on(self):
"""Return true if light is on."""
try:
return self._hm_get_state() > 0
@@ -98,7 +98,7 @@ class HMLight(HMDevice, LightEntity):
return features
@property
def hs_color(self) -> tuple[float, float] | None:
def hs_color(self):
"""Return the hue and saturation color value [float, float]."""
if ColorMode.HS not in self.supported_color_modes:
return None
@@ -116,14 +116,14 @@ class HMLight(HMDevice, LightEntity):
)
@property
def effect_list(self) -> list[str] | None:
def effect_list(self):
"""Return the list of supported effects."""
if not self.supported_features & LightEntityFeature.EFFECT:
return None
return self._hmdevice.get_effect_list()
@property
def effect(self) -> str | None:
def effect(self):
"""Return the current color change program of the light."""
if not self.supported_features & LightEntityFeature.EFFECT:
return None
@@ -166,7 +166,7 @@ class HMLight(HMDevice, LightEntity):
self._hmdevice.off(self._channel)
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata."""
# Use LEVEL
self._state = "LEVEL"

View File

@@ -48,7 +48,7 @@ class HMLock(HMDevice, LockEntity):
"""Open the door latch."""
self._hmdevice.open()
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate the data dictionary (self._data) from metadata."""
self._state = "STATE"
self._data.update({self._state: None})

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.components.notify import (
@@ -62,7 +60,7 @@ class HomematicNotificationService(BaseNotificationService):
self.hass = hass
self.data = data
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a notification to the device."""
data = {**self.data, **kwargs.get(ATTR_DATA, {})}

View File

@@ -339,7 +339,7 @@ class HMSensor(HMDevice, SensorEntity):
# No cast, return original value
return self._hm_get_state()
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate a data dictionary (self._data) from metadata."""
if self._state:
self._data.update({self._state: None})

View File

@@ -35,7 +35,7 @@ class HMSwitch(HMDevice, SwitchEntity):
"""Representation of a HomeMatic switch."""
@property
def is_on(self) -> bool:
def is_on(self):
"""Return True if switch is on."""
try:
return self._hm_get_state() > 0
@@ -43,7 +43,7 @@ class HMSwitch(HMDevice, SwitchEntity):
return False
@property
def today_energy_kwh(self) -> float | None:
def today_energy_kwh(self):
"""Return the current power usage in kWh."""
if "ENERGY_COUNTER" in self._data:
try:
@@ -61,7 +61,7 @@ class HMSwitch(HMDevice, SwitchEntity):
"""Turn the switch off."""
self._hmdevice.off(self._channel)
def _init_data_struct(self) -> None:
def _init_data_struct(self):
"""Generate the data dictionary (self._data) from metadata."""
self._state = "STATE"
self._data.update({self._state: None})

View File

@@ -9,7 +9,6 @@ from http import HTTPStatus
import json
import logging
import time
from typing import Any
from urllib.parse import urlparse
import uuid
@@ -452,7 +451,7 @@ class HTML5NotificationService(BaseNotificationService):
"""
await self.hass.async_add_executor_job(partial(self.dismiss, **kwargs))
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
tag = str(uuid.uuid4())
payload = {

View File

@@ -181,7 +181,7 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): # pylint: disable=hass
)
@property
def state_class(self) -> SensorStateClass:
def state_class(self):
"""Return the state class of this entity, from STATE_CLASSES, if any."""
return SensorStateClass.MEASUREMENT

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
from pyjoin import get_devices, send_notification
import voluptuous as vol
@@ -67,7 +66,7 @@ class JoinNotificationService(BaseNotificationService):
self._device_ids = device_ids
self._device_names = device_names
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA) or {}

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Any
from homeassistant.components.notify import ATTR_DATA, BaseNotificationService
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -29,7 +27,7 @@ class KebaNotificationService(BaseNotificationService):
"""Initialize the service."""
self._client = client
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
async def async_send_message(self, message="", **kwargs):
"""Send the message."""
text = message.replace(" ", "$") # Will be translated back by the display

View File

@@ -116,6 +116,7 @@ class KnxExposeOptions:
dpt: type[DPTBase]
respond_to_read: bool
cooldown: float
periodic_send: float
default: Any | None
value_template: Template | None
@@ -130,12 +131,17 @@ def _yaml_config_to_expose_options(config: ConfigType) -> KnxExposeOptions:
else:
dpt = DPTBase.parse_transcoder(config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]) # type: ignore[assignment] # checked by schema validation
ga = parse_device_group_address(config[KNX_ADDRESS])
cooldown_seconds = config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN].total_seconds()
periodic_send_seconds = config[
ExposeSchema.CONF_KNX_EXPOSE_PERIODIC_SEND
].total_seconds()
return KnxExposeOptions(
attribute=config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE),
group_address=ga,
dpt=dpt,
respond_to_read=config[CONF_RESPOND_TO_READ],
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
cooldown=cooldown_seconds,
periodic_send=periodic_send_seconds,
default=config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT),
value_template=config.get(CONF_VALUE_TEMPLATE),
)
@@ -167,6 +173,7 @@ class KnxExposeEntity:
respond_to_read=option.respond_to_read,
value_type=option.dpt,
cooldown=option.cooldown,
periodic_send=option.periodic_send,
),
)
for option in options

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.14.0",
"xknxproject==3.8.2",
"knx-frontend==2026.1.15.112308"
"knx-frontend==2025.12.30.151231"
],
"single_config_entry": true
}

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from abc import ABC
from collections import OrderedDict
from datetime import timedelta
import math
from typing import ClassVar, Final
@@ -538,6 +539,7 @@ class ExposeSchema(KNXPlatformSchema):
CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
CONF_KNX_EXPOSE_BINARY = "binary"
CONF_KNX_EXPOSE_COOLDOWN = "cooldown"
CONF_KNX_EXPOSE_PERIODIC_SEND = "periodic_send"
CONF_KNX_EXPOSE_DEFAULT = "default"
CONF_TIME = "time"
CONF_DATE = "date"
@@ -554,7 +556,12 @@ class ExposeSchema(KNXPlatformSchema):
)
EXPOSE_SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_KNX_EXPOSE_COOLDOWN, default=0): cv.positive_float,
vol.Optional(
CONF_KNX_EXPOSE_COOLDOWN, default=timedelta(0)
): cv.positive_time_period,
vol.Optional(
CONF_KNX_EXPOSE_PERIODIC_SEND, default=timedelta(0)
): cv.positive_time_period,
vol.Optional(CONF_RESPOND_TO_READ, default=True): cv.boolean,
vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(
CONF_KNX_EXPOSE_BINARY, sensor_type_validator

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
import aiohttp
import jsonrpc_async
@@ -94,7 +93,7 @@ class KodiNotificationService(BaseNotificationService):
self._server = jsonrpc_async.Server(self._url, **kwargs)
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
async def async_send_message(self, message="", **kwargs):
"""Send a message to Kodi."""
try:
data = kwargs.get(ATTR_DATA) or {}

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
import socket
from typing import Any
from urllib.parse import urlencode
import voluptuous as vol
@@ -74,7 +73,7 @@ class LannouncerNotificationService(BaseNotificationService):
self._host = host
self._port = port
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a message to Lannouncer."""
data = kwargs.get(ATTR_DATA)
if data is not None and ATTR_METHOD in data:

View File

@@ -1,14 +1,127 @@
"""Provides conditions for lights."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Final, Unpack, override
import voluptuous as vol
from homeassistant.const import CONF_OPTIONS, CONF_TARGET, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers import config_validation as cv, target
from homeassistant.helpers.condition import (
Condition,
ConditionChecker,
ConditionCheckParams,
ConditionConfig,
)
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
ATTR_BEHAVIOR: Final = "behavior"
BEHAVIOR_ANY: Final = "any"
BEHAVIOR_ALL: Final = "all"
STATE_CONDITION_VALID_STATES: Final = [STATE_ON, STATE_OFF]
STATE_CONDITION_OPTIONS_SCHEMA: dict[vol.Marker, Any] = {
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_ANY, BEHAVIOR_ALL]
),
}
STATE_CONDITION_SCHEMA = vol.Schema(
{
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
vol.Required(CONF_OPTIONS): STATE_CONDITION_OPTIONS_SCHEMA,
}
)
class StateConditionBase(Condition):
"""State condition."""
@override
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return STATE_CONDITION_SCHEMA(config) # type: ignore[no-any-return]
def __init__(
self, hass: HomeAssistant, config: ConditionConfig, state: str
) -> None:
"""Initialize condition."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.target
assert config.options
self._target = config.target
self._behavior = config.options[ATTR_BEHAVIOR]
self._state = state
@override
async def async_get_checker(self) -> ConditionChecker:
"""Get the condition checker."""
def check_any_match_state(states: list[str]) -> bool:
"""Test if any entity match the state."""
return any(state == self._state for state in states)
def check_all_match_state(states: list[str]) -> bool:
"""Test if all entities match the state."""
return all(state == self._state for state in states)
matcher: Callable[[list[str]], bool]
if self._behavior == BEHAVIOR_ANY:
matcher = check_any_match_state
elif self._behavior == BEHAVIOR_ALL:
matcher = check_all_match_state
def test_state(**kwargs: Unpack[ConditionCheckParams]) -> bool:
"""Test state condition."""
target_selection = target.TargetSelection(self._target)
targeted_entities = target.async_extract_referenced_entity_ids(
self._hass, target_selection, expand_group=False
)
referenced_entity_ids = targeted_entities.referenced.union(
targeted_entities.indirectly_referenced
)
light_entity_ids = {
entity_id
for entity_id in referenced_entity_ids
if split_entity_id(entity_id)[0] == DOMAIN
}
light_entity_states = [
state.state
for entity_id in light_entity_ids
if (state := self._hass.states.get(entity_id))
and state.state in STATE_CONDITION_VALID_STATES
]
return matcher(light_entity_states)
return test_state
class IsOnCondition(StateConditionBase):
"""Is on condition."""
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config, STATE_ON)
class IsOffCondition(StateConditionBase):
"""Is off condition."""
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config, STATE_OFF)
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
"is_off": IsOffCondition,
"is_on": IsOnCondition,
}

View File

@@ -1,4 +1,18 @@
.condition_common: &condition_common
is_off:
target:
entity:
domain: light
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_on:
target:
entity:
domain: light
@@ -12,6 +26,3 @@
options:
- all
- any
is_off: *condition_common
is_on: *condition_common

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from http import HTTPStatus
import logging
from typing import Any
import requests
import voluptuous as vol
@@ -57,7 +56,7 @@ class AutomateNotificationService(BaseNotificationService):
self._recipient = recipient
self._device = device
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
# Extract params from data dict

View File

@@ -7,6 +7,7 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["london_tube_status"],
"quality_scale": "legacy",
"requirements": ["london-tube-status==0.5"],
"single_config_entry": true
}

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
from pymailgunner import (
Client,
@@ -92,7 +91,7 @@ class MailgunNotificationService(BaseNotificationService):
return False
return True
def send_message(self, message: str = "", **kwargs: Any) -> None:
def send_message(self, message="", **kwargs):
"""Send a mail to the recipient."""
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)

View File

@@ -528,10 +528,7 @@ DISCOVERY_SCHEMAS = [
),
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.Thermostat.Attributes.RemoteSensing,
clusters.Thermostat.Attributes.OutdoorTemperature,
),
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
allow_multi=True,
),
MatterDiscoverySchema(

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