mirror of
https://github.com/home-assistant/core.git
synced 2026-05-07 10:26:51 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ef88f550f | |||
| c283826369 | |||
| 13565f5c94 | |||
| c208d68292 | |||
| 8bdb5e7a3c | |||
| d8d8bb23a5 | |||
| 5a10e105a8 | |||
| 65a68c138c | |||
| 8237c4db12 | |||
| d380ff61a5 | |||
| 0473407d38 | |||
| f7aecb654b | |||
| f7a91721dc | |||
| d200e547e1 |
@@ -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
-3
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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,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
Reference in New Issue
Block a user