Compare commits

..

14 Commits

Author SHA1 Message Date
Erik 7ef88f550f Adjust compound conditions 2026-04-23 17:17:36 +02:00
Erik c283826369 Update websocket_api.handle_test_condition 2026-04-23 17:15:39 +02:00
Erik 13565f5c94 Adjust EntityConditionBase 2026-04-23 17:15:39 +02:00
Erik c208d68292 Update compound conditions 2026-04-23 17:15:39 +02:00
Erik 8bdb5e7a3c Add duration support to cover conditions 2026-04-23 17:15:39 +02:00
Erik d8d8bb23a5 Add state tracking to EntityConditionBase 2026-04-23 17:15:39 +02:00
Erik 5a10e105a8 Migrate compound conditions to ConditionChecker 2026-04-23 17:15:39 +02:00
Erik 65a68c138c Reintroduce ConditionCheckParams 2026-04-23 17:14:48 +02:00
Erik 8237c4db12 Adjust 2026-04-23 16:52:25 +02:00
Erik d380ff61a5 Address review comments 2026-04-23 16:42:16 +02:00
Erik 0473407d38 Add ConditionChecker.async_on_unload 2026-04-23 16:26:03 +02:00
Erik f7aecb654b Log exceptions in cleanup 2026-04-23 13:48:00 +02:00
Erik f7a91721dc Adjust according to feedback 2026-04-23 07:50:05 +02:00
Erik d200e547e1 Refactor condition API 2026-04-22 13:28:44 +02:00
301 changed files with 1915 additions and 3952 deletions
+4 -5
View File
@@ -27,13 +27,12 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- List specific comments for each file/line that needs attention
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -1,5 +1,5 @@
---
name: ha-integration-knowledge
name: Home Assistant Integration knowledge
description: Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
---
@@ -14,8 +14,6 @@ description: Everything you need to know to build, test and review Home Assistan
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
- "potato" is a forbidden word for an integration and should never be used.
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
+1 -1
View File
@@ -38,4 +38,4 @@ When validation guarantees a dict key exists, prefer direct key access (`data["k
# Skills
- ha-integration-knowledge: .claude/skills/ha-integration-knowledge/SKILL.md
- Home Assistant Integration knowledge: .claude/skills/integrations/SKILL.md
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:python"
@@ -945,10 +945,7 @@ class PipelineRun:
try:
# Transcribe audio stream
stt_vad: VoiceCommandSegmenter | None = None
if (
self.audio_settings.is_vad_enabled
and self.stt_provider.audio_processing.requires_external_vad
):
if self.audio_settings.is_vad_enabled:
stt_vad = VoiceCommandSegmenter(
silence_seconds=self.audio_settings.silence_seconds
)
@@ -1,5 +1,4 @@
"""The Broadlink integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -34,8 +34,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink climate entities."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
if device.api.type in DOMAINS_AND_TYPES[Platform.CLIMATE]:
@@ -133,8 +133,6 @@ class BroadlinkDevice[_ApiT: blk.Device = blk.Device]:
await coordinator.async_config_entry_first_refresh()
self.update_manager = update_manager
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
self.hass.data[DOMAIN].devices[config.entry_id] = self
self.reset_jobs.append(config.add_update_listener(self.async_update))
@@ -32,8 +32,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink light."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
lights = []
@@ -95,8 +95,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Broadlink remote."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
remote = BroadlinkRemote(
device,
@@ -31,8 +31,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink select."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkDayOfWeek(device)])
@@ -108,8 +108,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink sensor."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
sensor_data = device.update_manager.coordinator.data
sensors = [
@@ -1,5 +1,4 @@
"""Support for Broadlink switches."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -22,8 +22,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink time."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkTime(device)])
@@ -1,5 +1,4 @@
"""Component to embed Google Cast."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
-2
View File
@@ -65,8 +65,6 @@ class ChromecastInfo:
"""
cast_info = self.cast_info
if self.cast_info.cast_type is None or self.cast_info.manufacturer is None:
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
unknown_models = hass.data[DOMAIN]["unknown_models"]
if self.cast_info.model_name not in unknown_models:
# Manufacturer and cast type is not available in mDNS data,
@@ -1,5 +1,4 @@
"""Provide functionality to interact with Cast devices on the network."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
+56 -56
View File
@@ -9,34 +9,34 @@
},
"conditions": {
"is_cooling": {
"description": "Tests if one or more thermostats are cooling.",
"description": "Tests if one or more climate-control devices are cooling.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Thermostat is cooling"
"name": "Climate-control device is cooling"
},
"is_drying": {
"description": "Tests if one or more thermostats are drying.",
"description": "Tests if one or more climate-control devices are drying.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Thermostat is drying"
"name": "Climate-control device is drying"
},
"is_heating": {
"description": "Tests if one or more thermostats are heating.",
"description": "Tests if one or more climate-control devices are heating.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Thermostat is heating"
"name": "Climate-control device is heating"
},
"is_hvac_mode": {
"description": "Tests if one or more thermostats are set to a specific HVAC mode.",
"description": "Tests if one or more climate-control devices are set to a specific HVAC mode.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -46,10 +46,10 @@
"name": "Modes"
}
},
"name": "Thermostat HVAC mode"
"name": "Climate-control device HVAC mode"
},
"is_off": {
"description": "Tests if one or more thermostats are off.",
"description": "Tests if one or more climate-control devices are off.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -58,19 +58,19 @@
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Thermostat is off"
"name": "Climate-control device is off"
},
"is_on": {
"description": "Tests if one or more thermostats are on.",
"description": "Tests if one or more climate-control devices are on.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Thermostat is on"
"name": "Climate-control device is on"
},
"target_humidity": {
"description": "Tests the humidity setpoint of one or more thermostats.",
"description": "Tests the humidity setpoint of one or more climate-control devices.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -79,10 +79,10 @@
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
},
"name": "Thermostat target humidity"
"name": "Climate-control device target humidity"
},
"target_temperature": {
"description": "Tests the temperature setpoint of one or more thermostats.",
"description": "Tests the temperature setpoint of one or more climate-control devices.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -91,7 +91,7 @@
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
},
"name": "Thermostat target temperature"
"name": "Climate-control device target temperature"
}
},
"device_automation": {
@@ -288,67 +288,67 @@
},
"services": {
"set_fan_mode": {
"description": "Sets the fan mode of a thermostat.",
"description": "Sets the fan mode of a climate-control device.",
"fields": {
"fan_mode": {
"description": "Fan operation mode.",
"name": "Fan mode"
}
},
"name": "Set thermostat fan mode"
"name": "Set climate-control device fan mode"
},
"set_humidity": {
"description": "Sets the target humidity of a thermostat.",
"description": "Sets the target humidity of a climate-control device.",
"fields": {
"humidity": {
"description": "Target humidity.",
"name": "Humidity"
}
},
"name": "Set thermostat target humidity"
"name": "Set climate-control device target humidity"
},
"set_hvac_mode": {
"description": "Sets the HVAC mode of a thermostat.",
"description": "Sets the HVAC mode of a climate-control device.",
"fields": {
"hvac_mode": {
"description": "HVAC operation mode.",
"name": "HVAC mode"
}
},
"name": "Set thermostat HVAC mode"
"name": "Set climate-control device HVAC mode"
},
"set_preset_mode": {
"description": "Sets the preset mode of a thermostat.",
"description": "Sets the preset mode of a climate-control device.",
"fields": {
"preset_mode": {
"description": "Preset mode.",
"name": "Preset mode"
}
},
"name": "Set thermostat preset mode"
"name": "Set climate-control device preset mode"
},
"set_swing_horizontal_mode": {
"description": "Sets the horizontal swing mode of a thermostat.",
"description": "Sets the horizontal swing mode of a climate-control device.",
"fields": {
"swing_horizontal_mode": {
"description": "Horizontal swing operation mode.",
"name": "Horizontal swing mode"
}
},
"name": "Set thermostat horizontal swing mode"
"name": "Set climate-control device horizontal swing mode"
},
"set_swing_mode": {
"description": "Sets the swing mode of a thermostat.",
"description": "Sets the swing mode of a climate-control device.",
"fields": {
"swing_mode": {
"description": "Swing operation mode.",
"name": "Swing mode"
}
},
"name": "Set thermostat swing mode"
"name": "Set climate-control device swing mode"
},
"set_temperature": {
"description": "Sets the target temperature of a thermostat.",
"description": "Sets the target temperature of a climate-control device.",
"fields": {
"hvac_mode": {
"description": "HVAC operation mode.",
@@ -367,25 +367,25 @@
"name": "Target temperature"
}
},
"name": "Set thermostat target temperature"
"name": "Set climate-control device target temperature"
},
"toggle": {
"description": "Toggles a thermostat on/off.",
"name": "Toggle thermostat"
"description": "Toggles a climate-control device on/off.",
"name": "Toggle climate-control device"
},
"turn_off": {
"description": "Turns off a thermostat.",
"name": "Turn off thermostat"
"description": "Turns off a climate-control device.",
"name": "Turn off climate-control device"
},
"turn_on": {
"description": "Turns on a thermostat.",
"name": "Turn on thermostat"
"description": "Turns on a climate-control device.",
"name": "Turn on climate-control device"
}
},
"title": "Climate",
"triggers": {
"hvac_mode_changed": {
"description": "Triggers after the mode of one or more thermostats changes.",
"description": "Triggers after the mode of one or more climate-control devices changes.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -398,10 +398,10 @@
"name": "Modes"
}
},
"name": "Thermostat mode changed"
"name": "Climate-control device mode changed"
},
"started_cooling": {
"description": "Triggers after one or more thermostats start cooling.",
"description": "Triggers after one or more climate-control devices start cooling.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -410,10 +410,10 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Thermostat started cooling"
"name": "Climate-control device started cooling"
},
"started_drying": {
"description": "Triggers after one or more thermostats start drying.",
"description": "Triggers after one or more climate-control devices start drying.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -422,10 +422,10 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Thermostat started drying"
"name": "Climate-control device started drying"
},
"started_heating": {
"description": "Triggers after one or more thermostats start heating.",
"description": "Triggers after one or more climate-control devices start heating.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -434,19 +434,19 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Thermostat started heating"
"name": "Climate-control device started heating"
},
"target_humidity_changed": {
"description": "Triggers after the humidity setpoint of one or more thermostats changes.",
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
"fields": {
"threshold": {
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Thermostat target humidity changed"
"name": "Climate-control device target humidity changed"
},
"target_humidity_crossed_threshold": {
"description": "Triggers after the humidity setpoint of one or more thermostats crosses a threshold.",
"description": "Triggers after the humidity setpoint of one or more climate-control devices crosses a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -458,19 +458,19 @@
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Thermostat target humidity crossed threshold"
"name": "Climate-control device target humidity crossed threshold"
},
"target_temperature_changed": {
"description": "Triggers after the temperature setpoint of one or more thermostats changes.",
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
"fields": {
"threshold": {
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Thermostat target temperature changed"
"name": "Climate-control device target temperature changed"
},
"target_temperature_crossed_threshold": {
"description": "Triggers after the temperature setpoint of one or more thermostats crosses a threshold.",
"description": "Triggers after the temperature setpoint of one or more climate-control devices crosses a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -482,10 +482,10 @@
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Thermostat target temperature crossed threshold"
"name": "Climate-control device target temperature crossed threshold"
},
"turned_off": {
"description": "Triggers after one or more thermostats turn off.",
"description": "Triggers after one or more climate-control devices turn off.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -494,10 +494,10 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Thermostat turned off"
"name": "Climate-control device turned off"
},
"turned_on": {
"description": "Triggers after one or more thermostats turn on, regardless of the mode.",
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -506,7 +506,7 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Thermostat turned on"
"name": "Climate-control device turned on"
}
}
}
@@ -169,8 +169,6 @@ class OptionsFlowHandler(OptionsFlowWithReload):
data_schema = vol.Schema(
{
# Polling interval is user-configurable, which is no longer allowed
# pylint: disable-next=hass-config-flow-polling-field
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
+6 -1
View File
@@ -4,7 +4,11 @@ from collections.abc import Mapping
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.condition import Condition, EntityConditionBase
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
Condition,
EntityConditionBase,
)
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
@@ -14,6 +18,7 @@ class CoverConditionBase(EntityConditionBase):
"""Base condition for cover state checks."""
_domain_specs: Mapping[str, CoverDomainSpec]
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state matches the expected cover state."""
@@ -8,6 +8,11 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
awning_is_closed:
fields: *condition_common_fields
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Awning is closed"
@@ -19,6 +23,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Awning is open"
@@ -28,6 +35,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Blind is closed"
@@ -37,6 +47,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Blind is open"
@@ -46,6 +59,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Curtain is closed"
@@ -55,6 +71,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Curtain is open"
@@ -64,6 +83,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Shade is closed"
@@ -73,6 +95,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Shade is open"
@@ -82,6 +107,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Shutter is closed"
@@ -91,6 +119,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::condition_for_name%]"
}
},
"name": "Shutter is open"
@@ -7,11 +7,10 @@ from typing import Any, Protocol
import voluptuous as vol
from homeassistant.const import CONF_DOMAIN, CONF_OPTIONS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import (
Condition,
ConditionChecker,
ConditionCheckerType,
ConditionConfig,
)
@@ -54,6 +53,7 @@ class DeviceCondition(Condition):
"""Device condition."""
_config: ConfigType
_platform_checker: ConditionCheckerType
@classmethod
async def async_validate_complete_config(
@@ -87,20 +87,20 @@ class DeviceCondition(Condition):
assert config.options is not None
self._config = config.options
async def async_get_checker(self) -> ConditionChecker:
async def async_setup(self) -> None:
"""Test a device condition."""
platform = await async_get_device_automation_platform(
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
)
platform_checker = platform.async_condition_from_config(
self._platform_checker = platform.async_condition_from_config(
self._hass, self._config
)
def checker(variables: TemplateVarsType = None, **kwargs: Any) -> bool:
result = platform_checker(self._hass, variables)
return result is not False
return checker
@callback
def _async_check(self, variables: TemplateVarsType = None, **kwargs: Any) -> bool:
"""Check the condition."""
result = self._platform_checker(self._hass, variables)
return result is not False
CONDITIONS: dict[str, type[Condition]] = {
@@ -1,5 +1,4 @@
"""Data used by this integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
-1
View File
@@ -1,5 +1,4 @@
"""Wrapper for media_source around async_upnp_client's DmsDevice ."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -8,6 +8,11 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_closed:
fields: *condition_common_fields
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::door::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::door::common::condition_for_name%]"
}
},
"name": "Door is closed"
@@ -19,6 +23,9 @@
"fields": {
"behavior": {
"name": "[%key:component::door::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::door::common::condition_for_name%]"
}
},
"name": "Door is open"
@@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
@@ -36,27 +35,6 @@ class DucoConfigFlow(ConfigFlow, domain=DOMAIN):
_host: str
_box_name: str
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
await self.async_set_unique_id(format_mac(discovery_info.macaddress))
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
try:
box_name, _ = await self._validate_input(discovery_info.ip)
except DucoConnectionError:
return self.async_abort(reason="cannot_connect")
except DucoError:
_LOGGER.exception("Unexpected error discovering Duco box via DHCP")
return self.async_abort(reason="unknown")
self._host = discovery_info.ip
self._box_name = box_name
self.context["title_placeholders"] = {"name": box_name}
return await self.async_step_discovery_confirm()
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
+1 -6
View File
@@ -3,17 +3,12 @@
"name": "Duco",
"codeowners": ["@ronaldvdmeer"],
"config_flow": true,
"dhcp": [
{
"hostname": "duco_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]"
}
],
"documentation": "https://www.home-assistant.io/integrations/duco",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.3.4"],
"requirements": ["python-duco-client==0.3.2"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
@@ -1,5 +1,4 @@
"""The EARN-E P1 Meter integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
+2 -44
View File
@@ -8,24 +8,18 @@ from aioesphomeapi import APIClient, APIConnectionError
from homeassistant.components import zeroconf
from homeassistant.components.bluetooth import async_remove_scanner
from homeassistant.components.usb import (
SerialDevice,
USBDevice,
async_register_serial_port_scanner,
)
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
__version__ as ha_version,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import async_delete_issue
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from . import assist_satellite, dashboard, ffmpeg_proxy, serial_proxy
from . import assist_satellite, dashboard, ffmpeg_proxy
from .const import CONF_BLUETOOTH_MAC_ADDRESS, CONF_NOISE_PSK, DOMAIN
from .domain_data import DomainData
from .encryption_key_storage import async_get_encryption_key_storage
@@ -40,48 +34,12 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
CLIENT_INFO = f"Home Assistant {ha_version}"
@callback
def _async_scan_serial_ports(
hass: HomeAssistant,
) -> list[USBDevice | SerialDevice]:
"""Return serial-proxy ports exposed by connected ESPHome devices."""
ports: list[USBDevice | SerialDevice] = []
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
entry_data = entry.runtime_data
if not entry_data.available:
continue
device_info = entry_data.device_info
if device_info is None:
continue
ports.extend(
SerialDevice(
device=str(serial_proxy.build_url(entry.entry_id, proxy.name)),
serial_number=(
device_info.mac_address.replace(":", "") + "-" + slugify(proxy.name)
),
manufacturer=device_info.manufacturer,
description=f"{device_info.model} ({proxy.name})",
)
for proxy in device_info.serial_proxies
)
return ports
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the esphome component."""
ffmpeg_proxy.async_setup(hass)
await assist_satellite.async_setup(hass)
await dashboard.async_setup(hass)
async_setup_websocket_api(hass)
if "usb" in hass.config.components:
async_register_serial_port_scanner(hass, _async_scan_serial_ports)
serial_proxy.set_hass_loop(hass.loop)
return True
@@ -40,7 +40,5 @@ class DomainData:
@cache
def get(cls, hass: HomeAssistant) -> Self:
"""Get the global DomainData instance stored in hass.data."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
ret = hass.data[DOMAIN] = cls()
return ret
@@ -1,7 +1,7 @@
{
"domain": "esphome",
"name": "ESPHome",
"after_dependencies": ["hassio", "tag", "usb", "zeroconf"],
"after_dependencies": ["hassio", "zeroconf", "tag"],
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
"config_flow": true,
"dependencies": ["assist_pipeline", "bluetooth", "intent", "ffmpeg", "http"],
@@ -1,113 +0,0 @@
"""Home Assistant-aware ESPHome serial proxy URI handler for serialx."""
from __future__ import annotations
import asyncio
from typing import cast
from aioesphomeapi import APIClient
from serialx import register_uri_handler
from serialx.platforms.serial_esphome import (
ESPHomeSerial,
ESPHomeSerialTransport,
InvalidSettingsError,
)
from yarl import URL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, async_get_hass
from .const import DOMAIN
from .entry_data import ESPHomeConfigEntry
SCHEME = "esphome-hass://"
# This is required so that serialx can safely query Core for an instance of an
# aioesphomeapi client. We cannot make any assumptions here, some packages run separate
# asyncio event loops in dedicated threads.
_HASS_LOOP: asyncio.AbstractEventLoop | None = None
def set_hass_loop(loop: asyncio.AbstractEventLoop) -> None:
"""Store a reference to the Core event loop."""
global _HASS_LOOP # noqa: PLW0603 # pylint: disable=global-statement
_HASS_LOOP = loop
def build_url(entry_id: str, port_name: str) -> URL:
"""Build a canonical `esphome-hass://` URL."""
return URL.build(
scheme="esphome-hass",
host="esphome",
path=f"/{entry_id}",
query={"port_name": port_name},
)
async def _resolve_client(entry_id: str) -> APIClient:
"""Look up the `APIClient` for a specific config entry."""
# This function is async specifically so that we can get a reference to the Home
# Assistant Core instance from its own thread
hass: HomeAssistant = async_get_hass()
entry = cast(ESPHomeConfigEntry, hass.config_entries.async_get_entry(entry_id))
if entry is None or entry.domain != DOMAIN:
raise InvalidSettingsError(f"No ESPHome config entry with id {entry_id!r}")
if entry.state is not ConfigEntryState.LOADED:
raise InvalidSettingsError(f"ESPHome config entry {entry_id!r} is not loaded")
return entry.runtime_data.client
class HassESPHomeSerial(ESPHomeSerial):
"""ESPHomeSerial that resolves an HA config entry's APIClient from the URL."""
_api: APIClient | None
_path: str | None
async def _async_open(self) -> None:
"""Resolve the HA config entry's APIClient, then open the proxy."""
if self._api is None and self._path is not None:
parsed = URL(str(self._path))
entry_id = parsed.path.lstrip("/")
if not entry_id:
raise InvalidSettingsError(
f"No ESPHome config entry id in URL {self._path!r}"
)
if "port_name" not in parsed.query:
raise InvalidSettingsError("Port name is required")
self._port_name = parsed.query["port_name"]
hass_loop = _HASS_LOOP
if hass_loop is None:
raise InvalidSettingsError(
"ESPHome integration has not registered its event loop"
)
# Fetch the `APIClient` from the Core via the appropriate event loop
self._api = await asyncio.wrap_future(
asyncio.run_coroutine_threadsafe(_resolve_client(entry_id), hass_loop)
)
self._client_loop = self._api._loop # noqa: SLF001
await super()._async_open()
class HassESPHomeSerialTransport(ESPHomeSerialTransport):
"""Transport variant that constructs :class:`HassESPHomeSerial`."""
transport_name = "esphome-hass"
_serial_cls = HassESPHomeSerial
register_uri_handler(
scheme=SCHEME,
unique_scheme=SCHEME,
sync_cls=HassESPHomeSerial,
async_transport_cls=HassESPHomeSerialTransport,
)
@@ -1,9 +1,4 @@
{
"common": {
"api_key": "Access token",
"api_key_description": "The access token for authenticating with Firefly III",
"verify_ssl_description": "Verify the SSL certificate of the Firefly III instance"
},
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
@@ -19,39 +14,39 @@
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:component::firefly_iii::common::api_key%]"
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::firefly_iii::common::api_key_description%]"
"api_key": "The new API access token for authenticating with Firefly III"
},
"description": "The access token for your Firefly III instance is invalid and needs to be updated. Go to **Options > Remote access and tokens**. Create a new **personal access token** and copy it (it will only display once)."
"description": "The access token for your Firefly III instance is invalid and needs to be updated. Go to **Options > Remote access and tokens**. Create a new personal access token and copy it (it will only display once)."
},
"reconfigure": {
"data": {
"api_key": "[%key:component::firefly_iii::common::api_key%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "[%key:component::firefly_iii::common::api_key_description%]",
"api_key": "[%key:component::firefly_iii::config::step::user::data_description::api_key%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:component::firefly_iii::common::verify_ssl_description%]"
"verify_ssl": "[%key:component::firefly_iii::config::step::user::data_description::verify_ssl%]"
},
"description": "Use the following form to reconfigure your Firefly III instance.",
"title": "Reconfigure Firefly III Integration"
},
"user": {
"data": {
"api_key": "[%key:component::firefly_iii::common::api_key%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "[%key:component::firefly_iii::common::api_key_description%]",
"api_key": "The API key for authenticating with Firefly III",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:component::firefly_iii::common::verify_ssl_description%]"
"verify_ssl": "Verify the SSL certificate of the Firefly III instance"
},
"description": "You can create an access token in the Firefly III UI. Go to **Options > Remote access and tokens**. Create a new **personal access token** and copy it (it will only display once)."
"description": "You can create an API key in the Firefly III UI. Go to **Options > Remote access and tokens**. Create a new personal access token and copy it (it will only display once)."
}
}
},
@@ -87,7 +87,8 @@ def async_wifi_bulb_for_host(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the flux_led component."""
hass.data[FLUX_LED_DISCOVERY] = []
domain_data = hass.data.setdefault(DOMAIN, {})
domain_data[FLUX_LED_DISCOVERY] = []
@callback
def _async_start_background_discovery(*_: Any) -> None:
+1 -3
View File
@@ -9,10 +9,8 @@ from flux_led.const import (
COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW,
COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW,
)
from flux_led.scanner import FluxLEDDiscovery
from homeassistant.components.light import ColorMode
from homeassistant.util.hass_dict import HassKey
DOMAIN: Final = "flux_led"
@@ -36,7 +34,7 @@ DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120
DEFAULT_SCAN_INTERVAL: Final = 5
DEFAULT_EFFECT_SPEED: Final = 50
FLUX_LED_DISCOVERY: HassKey[list[FluxLEDDiscovery]] = HassKey(DOMAIN)
FLUX_LED_DISCOVERY: Final = "flux_led_discovery"
FLUX_LED_EXCEPTIONS: Final = (
TimeoutError,
@@ -153,7 +153,8 @@ def async_update_entry_from_discovery(
@callback
def async_get_discovery(hass: HomeAssistant, host: str) -> FluxLEDDiscovery | None:
"""Check if a device was already discovered via a broadcast discovery."""
for discovery in hass.data[FLUX_LED_DISCOVERY]:
discoveries: list[FluxLEDDiscovery] = hass.data[DOMAIN][FLUX_LED_DISCOVERY]
for discovery in discoveries:
if discovery[ATTR_IPADDR] == host:
return discovery
return None
@@ -162,10 +163,10 @@ def async_get_discovery(hass: HomeAssistant, host: str) -> FluxLEDDiscovery | No
@callback
def async_clear_discovery_cache(hass: HomeAssistant, host: str) -> None:
"""Clear the host from the discovery cache."""
hass.data[FLUX_LED_DISCOVERY] = [
discovery
for discovery in hass.data[FLUX_LED_DISCOVERY]
if discovery[ATTR_IPADDR] != host
domain_data = hass.data[DOMAIN]
discoveries: list[FluxLEDDiscovery] = domain_data[FLUX_LED_DISCOVERY]
domain_data[FLUX_LED_DISCOVERY] = [
discovery for discovery in discoveries if discovery[ATTR_IPADDR] != host
]
@@ -2,14 +2,11 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, Concatenate
from typing import Any
from afsapi import (
AFSAPI,
FSApiError,
FSConnectionError,
FSNotImplementedError,
PlayCaps,
@@ -27,7 +24,6 @@ from homeassistant.components.media_player import (
RepeatMode,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
@@ -39,37 +35,6 @@ from .const import DOMAIN, MEDIA_CONTENT_ID_PRESET
_LOGGER = logging.getLogger(__name__)
def fs_command_exception_wrap[
_AFSAPIDeviceT: AFSAPIDevice,
**_P,
_R,
](
func: Callable[Concatenate[_AFSAPIDeviceT, _P], Awaitable[_R]],
) -> Callable[Concatenate[_AFSAPIDeviceT, _P], Coroutine[Any, Any, _R]]:
"""Wrap command methods and map API exceptions to HA errors."""
@wraps(func)
async def _wrap(self: _AFSAPIDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
try:
return await func(self, *args, **kwargs)
except FSConnectionError as err:
command = func.__name__.removeprefix("async_")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={"command": command},
) from err
except FSApiError as err:
command = func.__name__.removeprefix("async_")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"command": command, "message": str(err)},
) from err
return _wrap
async def async_setup_entry(
hass: HomeAssistant,
config_entry: FrontierSiliconConfigEntry,
@@ -307,17 +272,14 @@ class AFSAPIDevice(MediaPlayerEntity):
# Management actions
# power control
@fs_command_exception_wrap
async def async_turn_on(self) -> None:
"""Turn on the device."""
await self.fs_device.set_power(True)
@fs_command_exception_wrap
async def async_turn_off(self) -> None:
"""Turn off the device."""
await self.fs_device.set_power(False)
@fs_command_exception_wrap
async def async_media_play(self) -> None:
"""Send play command."""
if (await self.fs_device.get_play_state()) == PlayState.STOPPED:
@@ -327,54 +289,45 @@ class AFSAPIDevice(MediaPlayerEntity):
else:
await self.fs_device.play()
@fs_command_exception_wrap
async def async_media_pause(self) -> None:
"""Send pause command."""
await self.fs_device.pause()
@fs_command_exception_wrap
async def async_media_stop(self) -> None:
"""Send stop command."""
await self.fs_device.stop()
@fs_command_exception_wrap
async def async_media_previous_track(self) -> None:
"""Send previous track command (results in rewind)."""
await self.fs_device.rewind()
@fs_command_exception_wrap
async def async_media_next_track(self) -> None:
"""Send next track command (results in fast-forward)."""
await self.fs_device.forward()
@fs_command_exception_wrap
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
await self.fs_device.set_mute(mute)
# volume
@fs_command_exception_wrap
async def async_volume_up(self) -> None:
"""Send volume up command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) + 1
await self.fs_device.set_volume(min(volume, self._max_volume or 1))
@fs_command_exception_wrap
async def async_volume_down(self) -> None:
"""Send volume down command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) - 1
await self.fs_device.set_volume(max(volume, 0))
@fs_command_exception_wrap
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume command."""
if self._max_volume: # Can't do anything sensible if not set
volume = int(volume * self._max_volume)
await self.fs_device.set_volume(volume)
@fs_command_exception_wrap
async def async_select_source(self, source: str) -> None:
"""Select input source."""
await self.fs_device.set_power(True)
@@ -384,7 +337,6 @@ class AFSAPIDevice(MediaPlayerEntity):
):
await self.fs_device.set_mode(mode)
@fs_command_exception_wrap
async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Select EQ Preset."""
if (
@@ -393,7 +345,6 @@ class AFSAPIDevice(MediaPlayerEntity):
):
await self.fs_device.set_eq_preset(mode)
@fs_command_exception_wrap
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set repeat mode."""
await self.fs_device.play_repeat(
@@ -404,12 +355,10 @@ class AFSAPIDevice(MediaPlayerEntity):
}.get(repeat, PlayRepeatMode.OFF)
)
@fs_command_exception_wrap
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set shuffle mode."""
await self.fs_device.set_play_shuffle(shuffle)
@fs_command_exception_wrap
async def async_media_seek(self, position: float) -> None:
"""Seek to a position in seconds."""
await self.fs_device.set_play_position(int(position * 1000))
@@ -425,7 +374,6 @@ class AFSAPIDevice(MediaPlayerEntity):
return await browse_node(self.fs_device, media_content_type, media_content_id)
@fs_command_exception_wrap
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
@@ -33,13 +33,5 @@
}
}
}
},
"exceptions": {
"api_error": {
"message": "Failed to execute {command}: {message}"
},
"connection_error": {
"message": "Failed to execute {command}: could not connect to device"
}
}
}
@@ -8,6 +8,11 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_closed:
fields: *condition_common_fields
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::garage_door::common::condition_for_name%]"
}
},
"name": "Garage door is closed"
@@ -19,6 +23,9 @@
"fields": {
"behavior": {
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::garage_door::common::condition_for_name%]"
}
},
"name": "Garage door is open"
@@ -8,6 +8,11 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_closed:
fields: *condition_common_fields
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::gate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::gate::common::condition_for_name%]"
}
},
"name": "Gate is closed"
@@ -19,6 +23,9 @@
"fields": {
"behavior": {
"name": "[%key:component::gate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::gate::common::condition_for_name%]"
}
},
"name": "Gate is open"
@@ -1,5 +1,4 @@
"""Support for Actions on Google Assistant Smart Home Control."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -21,8 +21,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the platform."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
yaml_config: ConfigType = hass.data[DOMAIN][DATA_CONFIG]
google_config = config_entry.runtime_data
@@ -54,8 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -
Platform.NOTIFY,
DOMAIN,
{DATA_AUTH: auth, CONF_NAME: entry.title},
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
hass.data[DOMAIN][DATA_HASS_CONFIG],
)
)
@@ -1,5 +1,4 @@
"""The Hisense AEH-W4A1 integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import ipaddress
import logging
@@ -1,5 +1,4 @@
"""Pyaehw4a1 platform to control of Hisense AEH-W4A1 Climate Devices."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -219,8 +219,6 @@ class HiveOptionsFlowHandler(OptionsFlow):
schema = vol.Schema(
{
# Polling interval is user-configurable, which is no longer allowed
# pylint: disable-next=hass-config-flow-polling-field
vol.Optional(CONF_SCAN_INTERVAL, default=self.interval): vol.All(
vol.Coerce(int), vol.Range(min=30)
)
+36 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
from datetime import timedelta
from ipaddress import ip_address
import logging
import secrets
import time
@@ -23,14 +24,16 @@ from yarl import URL
from homeassistant.auth import jwt_wrapper
from homeassistant.auth.const import GROUP_ID_READ_ONLY
from homeassistant.auth.models import User
from homeassistant.components import websocket_api
from homeassistant.const import HASSIO_USER_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.http import current_request
from homeassistant.helpers.json import json_bytes
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.helpers.storage import Store
from homeassistant.util.network import is_local
from .auth_util import async_user_not_allowed_do_auth
from .const import (
KEY_AUTHENTICATED,
KEY_HASS_REFRESH_TOKEN_ID,
@@ -96,6 +99,38 @@ def async_sign_path(
return f"{url.path}?{url.query_string}"
@callback
def async_user_not_allowed_do_auth(
hass: HomeAssistant, user: User, request: Request | None = None
) -> str | None:
"""Validate that user is not allowed to do auth things."""
if not user.is_active:
return "User is not active"
if not user.local_only:
return None
# User is marked as local only, check if they are allowed to do auth
if request is None:
request = current_request.get()
if not request:
return "No request available to validate local access"
if is_cloud_connection(hass):
return "User is local only"
try:
remote_address = ip_address(request.remote) # type: ignore[arg-type]
except ValueError:
return "Invalid remote IP"
if is_local(remote_address):
return None
return "User cannot authenticate remotely"
async def async_setup_auth( # noqa: C901
hass: HomeAssistant,
app: Application,
@@ -1,45 +0,0 @@
"""Auth utilities for the HTTP component."""
from __future__ import annotations
from ipaddress import ip_address
from aiohttp.web import Request
from homeassistant.auth.models import User
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.http import current_request
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.util.network import is_local
@callback
def async_user_not_allowed_do_auth(
hass: HomeAssistant, user: User, request: Request | None = None
) -> str | None:
"""Validate that user is not allowed to do auth things."""
if not user.is_active:
return "User is not active"
if not user.local_only:
return None
# User is marked as local only, check if they are allowed to do auth
if request is None:
request = current_request.get()
if not request:
return "No request available to validate local access"
if is_cloud_connection(hass):
return "User is local only"
try:
remote_address = ip_address(request.remote) # type: ignore[arg-type]
except ValueError:
return "Invalid remote IP"
if is_local(remote_address):
return None
return "User cannot authenticate remotely"
+23 -23
View File
@@ -8,7 +8,7 @@ from contextlib import suppress
from dataclasses import dataclass, field
from datetime import timedelta
import logging
from typing import Any, cast
from typing import Any, NamedTuple, cast
from xml.parsers.expat import ExpatError
from huawei_lte_api.Client import Client
@@ -63,7 +63,6 @@ from .const import (
DEFAULT_MANUFACTURER,
DEFAULT_NOTIFY_SERVICE_NAME,
DOMAIN,
HUAWEI_LTE_CONFIG,
KEY_DEVICE_BASIC_INFORMATION,
KEY_DEVICE_INFORMATION,
KEY_DEVICE_SIGNAL,
@@ -108,7 +107,7 @@ class Router:
"""Class for router state."""
hass: HomeAssistant
config_entry: HuaweiLteConfigEntry
config_entry: ConfigEntry
connection: Connection
url: str
@@ -278,10 +277,14 @@ class Router:
self.connection.requests_session.close()
type HuaweiLteConfigEntry = ConfigEntry[Router]
class HuaweiLteData(NamedTuple):
"""Shared state."""
hass_config: ConfigType
routers: dict[str, Router]
async def async_setup_entry(hass: HomeAssistant, entry: HuaweiLteConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Huawei LTE component from config entry."""
url = entry.data[CONF_URL]
@@ -348,7 +351,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HuaweiLteConfigEntry) ->
return False
# Store reference to router
entry.runtime_data = router
hass.data[DOMAIN].routers[entry.entry_id] = router
# Clear all subscriptions, enabled entities will push back theirs
router.subscriptions.clear()
@@ -413,7 +416,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HuaweiLteConfigEntry) ->
CONF_NAME: entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME),
CONF_RECIPIENT: entry.options.get(CONF_RECIPIENT),
},
hass.data[HUAWEI_LTE_CONFIG],
hass.data[DOMAIN].hass_config,
)
def _update_router(*_: Any) -> None:
@@ -436,16 +439,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HuaweiLteConfigEntry) ->
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: HuaweiLteConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload config entry."""
# Forward config entry unload to platforms
await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
# Invoke router cleanup
await hass.async_add_executor_job(config_entry.runtime_data.cleanup)
# Forget about the router and invoke its cleanup
router = hass.data[DOMAIN].routers.pop(config_entry.entry_id)
await hass.async_add_executor_job(router.cleanup)
return True
@@ -453,7 +455,8 @@ async def async_unload_entry(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Huawei LTE component."""
hass.data[HUAWEI_LTE_CONFIG] = config
if DOMAIN not in hass.data:
hass.data[DOMAIN] = HuaweiLteData(hass_config=config, routers={})
def service_handler(service: ServiceCall) -> None:
"""Apply a service.
@@ -461,22 +464,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
We key this using the router URL instead of its unique id / serial number,
because the latter is not available anywhere in the UI.
"""
routers = [
entry.runtime_data
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
]
routers = hass.data[DOMAIN].routers
if url := service.data.get(CONF_URL):
router = next((router for router in routers if router.url == url), None)
router = next(
(router for router in routers.values() if router.url == url), None
)
elif not routers:
_LOGGER.error("%s: no routers configured", service.service)
return
elif len(routers) == 1:
router = routers[0]
router = next(iter(routers.values()))
else:
_LOGGER.error(
"%s: more than one router configured, must specify one of URLs %s",
service.service,
sorted(router.url for router in routers),
sorted(router.url for router in routers.values()),
)
return
if not router:
@@ -506,9 +508,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_migrate_entry(
hass: HomeAssistant, config_entry: HuaweiLteConfigEntry
) -> bool:
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate config entry to new version."""
if config_entry.version == 1:
options = dict(config_entry.options)
@@ -12,12 +12,13 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HuaweiLteConfigEntry
from .const import (
DOMAIN,
KEY_MONITORING_CHECK_NOTIFICATIONS,
KEY_MONITORING_STATUS,
KEY_WLAN_WIFI_FEATURE_SWITCH,
@@ -29,11 +30,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up from config entry."""
router = config_entry.runtime_data
router = hass.data[DOMAIN].routers[config_entry.entry_id]
entities: list[Entity] = []
if router.data.get(KEY_MONITORING_STATUS):
@@ -11,11 +11,12 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from . import HuaweiLteConfigEntry
from .const import DOMAIN
from .entity import HuaweiLteBaseEntityWithDevice
_LOGGER = logging.getLogger(__name__)
@@ -23,11 +24,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
async_add_entities: entity_platform.AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Huawei LTE buttons."""
router = config_entry.runtime_data
router = hass.data[DOMAIN].routers[config_entry.entry_id]
buttons = [
ClearTrafficStatisticsButton(router),
RestartButton(router),
@@ -21,7 +21,12 @@ from requests.exceptions import SSLError, Timeout
from url_normalize import url_normalize
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import (
CONF_MAC,
CONF_NAME,
@@ -42,7 +47,6 @@ from homeassistant.helpers.service_info.ssdp import (
SsdpServiceInfo,
)
from . import HuaweiLteConfigEntry
from .const import (
CONF_MANUFACTURER,
CONF_TRACK_WIRED_CLIENTS,
@@ -72,7 +76,7 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
) -> HuaweiLteOptionsFlow:
"""Get options flow."""
return HuaweiLteOptionsFlow()
@@ -1,12 +1,7 @@
"""Huawei LTE constants."""
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
DOMAIN = "huawei_lte"
HUAWEI_LTE_CONFIG: HassKey[ConfigType] = HassKey(DOMAIN)
CONF_MANUFACTURER = "manufacturer"
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode"
@@ -9,6 +9,7 @@ from homeassistant.components.device_tracker import (
DOMAIN as DEVICE_TRACKER_DOMAIN,
ScannerEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -16,10 +17,11 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import snakecase
from . import HuaweiLteConfigEntry, Router
from . import Router
from .const import (
CONF_TRACK_WIRED_CLIENTS,
DEFAULT_TRACK_WIRED_CLIENTS,
DOMAIN,
KEY_LAN_HOST_INFO,
KEY_WLAN_HOST_LIST,
UPDATE_SIGNAL,
@@ -48,7 +50,7 @@ def _get_hosts(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up from config entry."""
@@ -56,7 +58,7 @@ async def async_setup_entry(
# Grab hosts list once to examine whether the initial fetch has got some data for
# us, i.e. if wlan host list is supported. Only set up a subscription and proceed
# with adding and tracking entities if it is.
router = config_entry.runtime_data
router = hass.data[DOMAIN].routers[config_entry.entry_id]
if (hosts := _get_hosts(router, True)) is None:
return
@@ -5,9 +5,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import HuaweiLteConfigEntry
from .const import DOMAIN
ENTRY_FIELDS_DATA_TO_REDACT = {
"mac",
@@ -73,13 +74,13 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: HuaweiLteConfigEntry
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return async_redact_data(
{
"entry": entry.data,
"router": entry.runtime_data.data,
"router": hass.data[DOMAIN].routers[entry.entry_id].data,
},
TO_REDACT,
)
@@ -12,7 +12,8 @@ from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_RECIPIENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import HuaweiLteConfigEntry, Router
from . import Router
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -26,11 +27,7 @@ async def async_get_service(
if discovery_info is None:
return None
entry: HuaweiLteConfigEntry | None = hass.config_entries.async_get_entry(
discovery_info[ATTR_CONFIG_ENTRY_ID]
)
assert entry is not None
router = entry.runtime_data
router = hass.data[DOMAIN].routers[discovery_info[ATTR_CONFIG_ENTRY_ID]]
default_targets = discovery_info[CONF_RECIPIENT] or []
return HuaweiLteSmsNotificationService(router, default_targets)
@@ -22,7 +22,7 @@ rules:
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
runtime-data: todo
test-before-configure: done
test-before-setup: done
unique-config-entry: done
@@ -6,7 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
import logging
from typing import Any
from huawei_lte_api.enums.net import LTEBandEnum, NetworkBandEnum, NetworkModeEnum
@@ -15,13 +14,14 @@ from homeassistant.components.select import (
SelectEntity,
SelectEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HuaweiLteConfigEntry, Router
from .const import KEY_NET_NET_MODE
from . import Router
from .const import DOMAIN, KEY_NET_NET_MODE
from .entity import HuaweiLteBaseEntityWithDevice
_LOGGER = logging.getLogger(__name__)
@@ -31,16 +31,16 @@ _LOGGER = logging.getLogger(__name__)
class HuaweiSelectEntityDescription(SelectEntityDescription):
"""Class describing Huawei LTE select entities."""
setter_fn: Callable[[str], Any]
setter_fn: Callable[[str], None]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up from config entry."""
router = config_entry.runtime_data
router = hass.data[DOMAIN].routers[config_entry.entry_id]
selects: list[Entity] = []
desc = HuaweiSelectEntityDescription(
@@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
@@ -30,8 +31,9 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import HuaweiLteConfigEntry, Router
from . import Router
from .const import (
DOMAIN,
KEY_DEVICE_INFORMATION,
KEY_DEVICE_SIGNAL,
KEY_MONITORING_CHECK_NOTIFICATIONS,
@@ -793,11 +795,11 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up from config entry."""
router = config_entry.runtime_data
router = hass.data[DOMAIN].routers[config_entry.entry_id]
sensors: list[Entity] = []
for key in SENSOR_KEYS:
if not (items := router.data.get(key)):
@@ -10,12 +10,16 @@ from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HuaweiLteConfigEntry
from .const import KEY_DIALUP_MOBILE_DATASWITCH, KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH
from .const import (
DOMAIN,
KEY_DIALUP_MOBILE_DATASWITCH,
KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH,
)
from .entity import HuaweiLteBaseEntityWithDevice
_LOGGER = logging.getLogger(__name__)
@@ -23,11 +27,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuaweiLteConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up from config entry."""
router = config_entry.runtime_data
router = hass.data[DOMAIN].routers[config_entry.entry_id]
switches: list[Entity] = []
if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH):
+1 -21
View File
@@ -494,7 +494,6 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
async def _async_wait_push_loop(self) -> None:
"""Wait for data push from server."""
idle: asyncio.Future | None = None
while True:
try:
self.number_of_messages = await self._async_fetch_number_of_messages()
@@ -528,9 +527,8 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
else:
self.auth_errors = 0
self.async_set_updated_data(self.number_of_messages)
try:
idle = await self.imap_client.idle_start()
idle: asyncio.Future = await self.imap_client.idle_start()
await self.imap_client.wait_server_push()
self.imap_client.idle_done()
async with asyncio.timeout(10):
@@ -545,24 +543,6 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
await self._cleanup()
await asyncio.sleep(BACKOFF_TIME)
finally:
# Ensure no pending IDLE future survives
if idle is not None and not idle.done():
idle.cancel()
_LOGGER.debug(
"Canceling IDLE wait for %s",
self.config_entry.data[CONF_SERVER],
)
try:
await idle
except asyncio.CancelledError:
if (
current_task := asyncio.current_task()
) and current_task.cancelling():
raise
except AioImapException:
pass
async def shutdown(self, *_: Any) -> None:
"""Close resources."""
if self._push_wait_task:
@@ -43,6 +43,7 @@ NUMBERS: Final = (
native_max_value=100,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
device_class=NumberDeviceClass.BATTERY,
),
IndevoltNumberEntityDescription(
key="max_ac_output_power",
+4 -2
View File
@@ -69,8 +69,10 @@ SENSORS: Final = (
IndevoltSensorEntityDescription(
key="6105",
generation=[1],
translation_key="discharge_limit",
native_unit_of_measurement=PERCENTAGE,
translation_key="rated_capacity",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
IndevoltSensorEntityDescription(
key="2101",
@@ -223,9 +223,6 @@
"dc_output_power": {
"name": "DC output power"
},
"discharge_limit": {
"name": "[%key:component::indevolt::entity::number::discharge_limit::name%]"
},
"energy_mode": {
"name": "Energy mode",
"state": {
@@ -1,5 +1,4 @@
"""Support for INSTEON Modems (PLM and Hub)."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from contextlib import suppress
import logging
-1
View File
@@ -1,5 +1,4 @@
"""Native Home Assistant iOS app component."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import datetime
from http import HTTPStatus
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from homeassistant.const import CONF_SCAN_INTERVAL, Platform
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -17,6 +17,7 @@ from .const import (
DEFAULT_CONSIDER_HOME,
DEFAULT_INTERFACE,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
)
from .router import KeeneticConfigEntry, KeeneticRouter
@@ -26,6 +27,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: KeeneticConfigEntry) -> bool:
"""Set up the component."""
hass.data.setdefault(DOMAIN, {})
async_add_defaults(hass, entry)
router = KeeneticRouter(hass, entry)
@@ -83,8 +85,10 @@ async def async_unload_entry(
return unload_ok
def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry) -> None:
def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry):
"""Populate default options."""
host: str = entry.data[CONF_HOST]
imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {})
options = {
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME,
@@ -92,6 +96,7 @@ def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry) -> None:
CONF_TRY_HOTSPOT: True,
CONF_INCLUDE_ARP: True,
CONF_INCLUDE_ASSOCIATED: True,
**imported_options,
**entry.options,
}
@@ -198,8 +198,6 @@ class KeeneticOptionsFlowHandler(OptionsFlowWithReload):
options = vol.Schema(
{
# Polling interval is user-configurable, which is no longer allowed
# pylint: disable-next=hass-config-flow-polling-field
vol.Required(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
@@ -1,5 +1,4 @@
"""Support for Konnected devices."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import copy
import hmac
@@ -24,8 +24,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors attached to a Konnected device from a config entry."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
data = hass.data[DOMAIN]
device_id = config_entry.data["id"]
sensors = [
@@ -1,5 +1,4 @@
"""Support for Konnected devices."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import asyncio
import logging
@@ -46,8 +46,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensors attached to a Konnected device from a config entry."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
data = hass.data[DOMAIN]
device_id = config_entry.data["id"]
@@ -1,5 +1,4 @@
"""Support for wired switches attached to a Konnected device."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import logging
from typing import Any
+17 -16
View File
@@ -2,38 +2,39 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DISPATCH_CONFIG_UPDATED
from .coordinator import KrakenConfigEntry, KrakenData
from .const import DISPATCH_CONFIG_UPDATED, DOMAIN
from .coordinator import KrakenData
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: KrakenConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up kraken from a config entry."""
kraken_data = KrakenData(hass, entry)
await kraken_data.async_setup()
entry.runtime_data = kraken_data
hass.data[DOMAIN] = kraken_data
entry.async_on_unload(entry.add_update_listener(async_options_updated))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: KrakenConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_options_updated(
hass: HomeAssistant, config_entry: KrakenConfigEntry
) -> None:
"""Triggered by config entry options updates."""
config_entry.runtime_data.set_update_interval(
config_entry.options[CONF_SCAN_INTERVAL]
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok:
hass.data.pop(DOMAIN)
return unload_ok
async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Triggered by config entry options updates."""
hass.data[DOMAIN].set_update_interval(config_entry.options[CONF_SCAN_INTERVAL])
async_dispatcher_send(hass, DISPATCH_CONFIG_UPDATED, hass, config_entry)
@@ -8,13 +8,17 @@ import krakenex
from pykrakenapi.pykrakenapi import KrakenAPI
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN
from .coordinator import KrakenConfigEntry
from .utils import get_tradable_asset_pairs
@@ -26,7 +30,7 @@ class KrakenConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: KrakenConfigEntry,
config_entry: ConfigEntry,
) -> KrakenOptionsFlowHandler:
"""Get the options flow for this handler."""
return KrakenOptionsFlowHandler()
@@ -75,8 +79,6 @@ class KrakenOptionsFlowHandler(OptionsFlow):
)
options = {
# Polling interval is user-configurable, which is no longer allowed
# pylint: disable-next=hass-config-flow-polling-field
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
@@ -28,13 +28,10 @@ CALL_RATE_LIMIT_SLEEP = 1
_LOGGER = logging.getLogger(__name__)
type KrakenConfigEntry = ConfigEntry[KrakenData]
class KrakenData:
"""Define an object to hold kraken data."""
def __init__(self, hass: HomeAssistant, config_entry: KrakenConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize."""
self._hass = hass
self._config_entry = config_entry
+5 -6
View File
@@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
@@ -27,7 +28,7 @@ from .const import (
DOMAIN,
KrakenResponse,
)
from .coordinator import KrakenConfigEntry, KrakenData
from .coordinator import KrakenData
_LOGGER = logging.getLogger(__name__)
@@ -137,7 +138,7 @@ SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: KrakenConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add kraken entities from a config_entry."""
@@ -148,7 +149,7 @@ async def async_setup_entry(
entities.extend(
[
KrakenSensor(
config_entry.runtime_data,
hass.data[DOMAIN],
tracked_asset_pair,
description,
)
@@ -160,9 +161,7 @@ async def async_setup_entry(
_async_add_kraken_sensors(config_entry.options[CONF_TRACKED_ASSET_PAIRS])
@callback
def async_update_sensors(
hass: HomeAssistant, config_entry: KrakenConfigEntry
) -> None:
def async_update_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Add or remove sensors for configured tracked asset pairs."""
dev_reg = dr.async_get(hass)
@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],
"requirements": ["thinqconnect==1.0.12"]
"requirements": ["thinqconnect==1.0.11"]
}
@@ -1,5 +1,4 @@
"""Support for LinkPlay devices."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from dataclasses import dataclass
@@ -1,5 +1,4 @@
"""Support for LinkPlay media players."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -1,5 +1,4 @@
"""Utilities for the LinkPlay component."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from aiohttp import ClientSession
from linkplay.utils import async_create_unverified_client_session
@@ -113,8 +113,6 @@ async def handle_webhook(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure based on config entry."""
if DOMAIN not in hass.data:
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
hass.data[DOMAIN] = {"devices": set(), "unsub_device_tracker": {}}
webhook.async_register(
hass, DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook
@@ -1,5 +1,4 @@
"""Support for the Locative platform."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
@@ -1,5 +1,4 @@
"""Support for Mailgun."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import hashlib
import hmac
@@ -44,8 +44,6 @@ def get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> MailgunNotificationService | None:
"""Get the Mailgun notification service."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
data = hass.data[DOMAIN]
mailgun_service = MailgunNotificationService(
data.get(CONF_DOMAIN),
@@ -1,5 +1,4 @@
"""The Matter integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -37,8 +37,6 @@ def get_matter(hass: HomeAssistant) -> MatterAdapter:
# NOTE: This assumes only one Matter connection/fabric can exist.
# Shall we support connecting to multiple servers in the client or by
# config entries? In case of the config entry we need to fix this.
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values()))
return matter_entry_data.adapter
@@ -1,5 +1,4 @@
"""Support for Meteo-France weather data."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import logging
@@ -58,8 +58,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await data_coordinator.async_config_entry_first_refresh()
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
hass.data[DOMAIN][conn_type][key] = data_coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-1
View File
@@ -1,5 +1,4 @@
"""Support for mill wifi-enabled home heaters."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from typing import Any
-2
View File
@@ -22,8 +22,6 @@ async def async_setup_entry(
) -> None:
"""Set up the Mill Number."""
if entry.data.get(CONNECTION_TYPE) == CLOUD:
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
mill_data_coordinator: MillDataUpdateCoordinator = hass.data[DOMAIN][CLOUD][
entry.data[CONF_USERNAME]
]
-1
View File
@@ -1,5 +1,4 @@
"""Support for mill wifi-enabled home heaters."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations
@@ -1,5 +1,4 @@
"""Integrates Native Apps to Home Assistant."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from contextlib import suppress
from functools import partial
@@ -110,8 +110,6 @@ class MobileAppEntity(RestoreEntity):
def _apply_pending_update(self) -> None:
"""Restore any pending update for this entity."""
entity_type = self._config[ATTR_SENSOR_TYPE]
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
pending_updates = self.hass.data[DOMAIN][DATA_PENDING_UPDATES][entity_type]
if update := pending_updates.pop(self._attr_unique_id, None):
_LOGGER.debug(

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