mirror of
https://github.com/home-assistant/core.git
synced 2026-02-06 23:34:51 +01:00
Compare commits
95 Commits
strings/ma
...
whirlpool_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e4a6d9092 | ||
|
|
c5b9699098 | ||
|
|
6937bfdf67 | ||
|
|
39ee3fcfaa | ||
|
|
16cdfd05a0 | ||
|
|
f49d4787be | ||
|
|
2076700dc4 | ||
|
|
76c135913e | ||
|
|
c3534d5445 | ||
|
|
fc60b16d65 | ||
|
|
0443c93f77 | ||
|
|
f97cf0e446 | ||
|
|
bd4fa0d5c2 | ||
|
|
f60d367184 | ||
|
|
6e231f2ec5 | ||
|
|
13ba2d2e47 | ||
|
|
ba4a163e24 | ||
|
|
b7db8684db | ||
|
|
a7595dc468 | ||
|
|
d2c8c3565b | ||
|
|
422d1031f4 | ||
|
|
c9a79cf100 | ||
|
|
c42d47a619 | ||
|
|
a26f871d32 | ||
|
|
d481c1bcc5 | ||
|
|
379e3596b4 | ||
|
|
423a7cdbba | ||
|
|
841fa48186 | ||
|
|
61e35157e3 | ||
|
|
87f655f56d | ||
|
|
692b8d0722 | ||
|
|
5f9f623c3f | ||
|
|
e595b6cd90 | ||
|
|
a748eebf3e | ||
|
|
6bdd544867 | ||
|
|
705eadf8ce | ||
|
|
b7c6e4eafc | ||
|
|
f4aba286fe | ||
|
|
5fa4f6de11 | ||
|
|
db1f045c42 | ||
|
|
eaba4817bd | ||
|
|
96cb2247df | ||
|
|
99fa7a1f52 | ||
|
|
e0ba928296 | ||
|
|
16fd5e8f1f | ||
|
|
201e95a417 | ||
|
|
dc01592991 | ||
|
|
c5fb2bd566 | ||
|
|
d03d996155 | ||
|
|
9618412a44 | ||
|
|
967e97661f | ||
|
|
b757312fe0 | ||
|
|
2ed8ec0bdf | ||
|
|
97f6e3741a | ||
|
|
c2d3244d26 | ||
|
|
eafeba792d | ||
|
|
c9318b6fbf | ||
|
|
99be382abf | ||
|
|
7cfcfca210 | ||
|
|
f29daccb19 | ||
|
|
be869fce6c | ||
|
|
7bb0414a39 | ||
|
|
3f8807d063 | ||
|
|
67642e6246 | ||
|
|
0d215597f3 | ||
|
|
f41bd2b582 | ||
|
|
5c9ec1911b | ||
|
|
1a0b7fe984 | ||
|
|
26ee25d7bb | ||
|
|
aabf52d3cf | ||
|
|
99fcb46a7e | ||
|
|
6580c5e5bf | ||
|
|
63e7d4dc08 | ||
|
|
cc6900d846 | ||
|
|
ca2ad22884 | ||
|
|
40944f0f2d | ||
|
|
91a3e488b1 | ||
|
|
9a1f517e6e | ||
|
|
c82c614bb9 | ||
|
|
20914dce67 | ||
|
|
5fc407d2f3 | ||
|
|
c7444d38a1 | ||
|
|
81f6136bda | ||
|
|
862d0ea49e | ||
|
|
f2fdfed241 | ||
|
|
15640049cb | ||
|
|
5c163434f8 | ||
|
|
e54c2ea55e | ||
|
|
1ec42693ab | ||
|
|
672864ae4f | ||
|
|
e54d7e42cb | ||
|
|
5d63fce015 | ||
|
|
190fe10eed | ||
|
|
ef410c1e2a | ||
|
|
5a712398e7 |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
|
||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -310,7 +310,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: &actions-cache actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: &actions-cache actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: &key-python-venv >-
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: &actions-cache-save actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: &actions-cache-save actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: *path-apt-cache
|
||||
key: *key-apt-cache
|
||||
@@ -425,7 +425,7 @@ jobs:
|
||||
steps:
|
||||
- &cache-restore-apt
|
||||
name: Restore apt cache
|
||||
uses: &actions-cache-restore actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: &actions-cache-restore actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: *path-apt-cache
|
||||
fail-on-cache-miss: true
|
||||
|
||||
@@ -389,6 +389,7 @@ homeassistant.components.onkyo.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.open_router.*
|
||||
homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openevse.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
|
||||
@@ -52,6 +52,9 @@ RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
|
||||
# Claude Code native install
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
|
||||
@@ -7,10 +7,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, SERVER_URL
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTRIBUTION = "ispyconnect.com"
|
||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||
@@ -19,6 +21,14 @@ PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
AgentDVRConfigEntry = ConfigEntry[Agent]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||
|
||||
@@ -9,10 +9,7 @@ from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
@@ -21,20 +18,6 @@ SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEV_EN_ALT = "enable_alerts"
|
||||
_DEV_DS_ALT = "disable_alerts"
|
||||
_DEV_EN_REC = "start_recording"
|
||||
_DEV_DS_REC = "stop_recording"
|
||||
_DEV_SNAP = "snapshot"
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_DEV_EN_ALT: "async_enable_alerts",
|
||||
_DEV_DS_ALT: "async_disable_alerts",
|
||||
_DEV_EN_REC: "async_start_recording",
|
||||
_DEV_DS_REC: "async_stop_recording",
|
||||
_DEV_SNAP: "async_snapshot",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -57,10 +40,6 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(cameras)
|
||||
|
||||
platform = async_get_current_platform()
|
||||
for service, method in CAMERA_SERVICES.items():
|
||||
platform.async_register_entity_service(service, None, method)
|
||||
|
||||
|
||||
class AgentCamera(MjpegCamera):
|
||||
"""Representation of an Agent Device Stream."""
|
||||
|
||||
38
homeassistant/components/agent_dvr/services.py
Normal file
38
homeassistant/components/agent_dvr/services.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Services for Agent DVR."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_DEV_EN_ALT = "enable_alerts"
|
||||
_DEV_DS_ALT = "disable_alerts"
|
||||
_DEV_EN_REC = "start_recording"
|
||||
_DEV_DS_REC = "stop_recording"
|
||||
_DEV_SNAP = "snapshot"
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_DEV_EN_ALT: "async_enable_alerts",
|
||||
_DEV_DS_ALT: "async_disable_alerts",
|
||||
_DEV_EN_REC: "async_start_recording",
|
||||
_DEV_DS_REC: "async_stop_recording",
|
||||
_DEV_SNAP: "async_snapshot",
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
for service_name, method in CAMERA_SERVICES.items():
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
service_name,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func=method,
|
||||
)
|
||||
@@ -158,9 +158,9 @@
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,12 +18,15 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_BAUD,
|
||||
CONF_DEVICE_PATH,
|
||||
DOMAIN,
|
||||
PROTOCOL_SERIAL,
|
||||
PROTOCOL_SOCKET,
|
||||
SIGNAL_PANEL_MESSAGE,
|
||||
@@ -32,9 +35,11 @@ from .const import (
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -54,6 +59,12 @@ class AlarmDecoderData:
|
||||
restart: bool
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AlarmDecoderConfigEntry
|
||||
) -> bool:
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -27,11 +23,6 @@ from .const import (
|
||||
)
|
||||
from .entity import AlarmDecoderEntity
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -50,23 +41,6 @@ async def async_setup_entry(
|
||||
)
|
||||
async_add_entities([entity])
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
{
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
},
|
||||
"alarm_toggle_chime",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
{
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
},
|
||||
"alarm_keypress",
|
||||
)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
46
homeassistant/components/alarmdecoder/services.py
Normal file
46
homeassistant/components/alarmdecoder/services.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
},
|
||||
func="alarm_toggle_chime",
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
},
|
||||
func="alarm_keypress",
|
||||
)
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
|
||||
"learn_more_url": "https://www.home-assistant.io/blog/2026/02/02/about-device-database/",
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -600,6 +600,16 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
system = chat_log.content[0]
|
||||
if not isinstance(system, conversation.SystemContent):
|
||||
raise TypeError("First message must be a system message")
|
||||
|
||||
# System prompt with caching enabled
|
||||
system_prompt: list[TextBlockParam] = [
|
||||
TextBlockParam(
|
||||
type="text",
|
||||
text=system.content,
|
||||
cache_control={"type": "ephemeral"},
|
||||
)
|
||||
]
|
||||
|
||||
messages = _convert_content(chat_log.content[1:])
|
||||
|
||||
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
@@ -608,7 +618,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||
system=system.content,
|
||||
system=system_prompt,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
@@ -695,10 +705,6 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
type="auto",
|
||||
)
|
||||
|
||||
if isinstance(model_args["system"], str):
|
||||
model_args["system"] = [
|
||||
TextBlockParam(type="text", text=model_args["system"])
|
||||
]
|
||||
model_args["system"].append( # type: ignore[union-attr]
|
||||
TextBlockParam(
|
||||
type="text",
|
||||
|
||||
@@ -540,7 +540,17 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
data = self.coordinator.data[key]
|
||||
|
||||
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
# The date could be "N/A" for certain fields (e.g., XOFFBATT), indicating there is no value yet.
|
||||
if data == "N/A":
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
try:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
except (dateutil.parser.ParserError, OverflowError):
|
||||
# If parsing fails we should mark it as unknown, with a log for further debugging.
|
||||
_LOGGER.warning('Failed to parse date for %s: "%s"', key, data)
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
self._attr_native_value, inferred_unit = infer_unit(data)
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Baidu speech service."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aip import AipSpeech
|
||||
import voluptuous as vol
|
||||
@@ -9,6 +10,7 @@ from homeassistant.components.tts import (
|
||||
CONF_LANG,
|
||||
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
|
||||
Provider,
|
||||
TtsAudioType,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -85,17 +87,17 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def default_language(self):
|
||||
def default_language(self) -> str:
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self):
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return a list of supported languages."""
|
||||
return SUPPORTED_LANGUAGES
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
"""Return a dict including default options."""
|
||||
return {
|
||||
CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]],
|
||||
@@ -105,11 +107,16 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_options(self):
|
||||
def supported_options(self) -> list[str]:
|
||||
"""Return a list of supported options."""
|
||||
return SUPPORTED_OPTIONS
|
||||
|
||||
def get_tts_audio(self, message, language, options):
|
||||
def get_tts_audio(
|
||||
self,
|
||||
message: str,
|
||||
language: str,
|
||||
options: dict[str, Any],
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from BaiduTTS."""
|
||||
|
||||
aip_speech = AipSpeech(
|
||||
|
||||
@@ -324,9 +324,9 @@
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,16 +6,9 @@ from typing import Any
|
||||
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.blinkpy import Blink
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import (
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -27,13 +20,6 @@ from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string}
|
||||
)
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILE_PATH): cv.string}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@@ -9,35 +9,23 @@ from typing import Any
|
||||
from blinkpy.auth import UnauthorizedError
|
||||
from blinkpy.camera import BlinkCamera as BlinkCameraAPI
|
||||
from requests.exceptions import ChunkedEncodingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
HomeAssistantError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
DEFAULT_BRAND,
|
||||
DOMAIN,
|
||||
SERVICE_RECORD,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_TRIGGER,
|
||||
)
|
||||
from .const import DEFAULT_BRAND, DOMAIN
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_VIDEO_CLIP = "video"
|
||||
ATTR_IMAGE = "image"
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@@ -56,20 +44,6 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(SERVICE_RECORD, None, "record")
|
||||
platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera")
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{vol.Required(CONF_FILE_PATH): cv.string},
|
||||
"save_recent_clips",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{vol.Required(CONF_FILENAME): cv.string},
|
||||
"save_video",
|
||||
)
|
||||
|
||||
|
||||
class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
@@ -20,11 +20,6 @@ TYPE_TEMPERATURE = "temperature"
|
||||
TYPE_BATTERY = "battery"
|
||||
TYPE_WIFI_STRENGTH = "wifi_strength"
|
||||
|
||||
SERVICE_RECORD = "record"
|
||||
SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
|
||||
@@ -4,13 +4,27 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir, service
|
||||
|
||||
from .const import DOMAIN, SERVICE_SEND_PIN
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_RECORD = "record"
|
||||
SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
|
||||
|
||||
# Deprecated
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
@@ -52,3 +66,36 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
_send_pin,
|
||||
schema=SERVICE_SEND_PIN_SCHEMA,
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RECORD,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func="record",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_TRIGGER,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func="trigger_camera",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(CONF_FILE_PATH): cv.string},
|
||||
func="save_recent_clips",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(CONF_FILENAME): cv.string},
|
||||
func="save_video",
|
||||
)
|
||||
|
||||
@@ -16,14 +16,17 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import BRIDGE_MAKE, DOMAIN
|
||||
from .models import BondData
|
||||
from .services import async_setup_services
|
||||
from .utils import BondHub
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
@@ -38,6 +41,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
type BondConfigEntry = ConfigEntry[BondData]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool:
|
||||
"""Set up Bond from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
@@ -5,10 +5,3 @@ BRIDGE_MAKE = "Olibra"
|
||||
DOMAIN = "bond"
|
||||
|
||||
CONF_BOND_ID: str = "bond_id"
|
||||
|
||||
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state"
|
||||
SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state"
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE = "set_light_power_tracked_state"
|
||||
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE = "set_light_brightness_tracked_state"
|
||||
ATTR_POWER_STATE = "power_state"
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from bond_async import Action, DeviceType, Direction
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
@@ -18,7 +17,6 @@ from homeassistant.components.fan import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
@@ -27,7 +25,6 @@ from homeassistant.util.percentage import (
|
||||
from homeassistant.util.scaling import int_states_in_range
|
||||
|
||||
from . import BondConfigEntry
|
||||
from .const import SERVICE_SET_FAN_SPEED_TRACKED_STATE
|
||||
from .entity import BondEntity
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
@@ -44,12 +41,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond fan devices."""
|
||||
data = entry.runtime_data
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
|
||||
{vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))},
|
||||
"async_set_speed_belief",
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
BondFan(data, device)
|
||||
|
||||
@@ -7,37 +7,20 @@ from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from bond_async import Action, DeviceType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BondConfigEntry
|
||||
from .const import (
|
||||
ATTR_POWER_STATE,
|
||||
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
|
||||
)
|
||||
from .entity import BondEntity
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_START_INCREASING_BRIGHTNESS = "start_increasing_brightness"
|
||||
SERVICE_START_DECREASING_BRIGHTNESS = "start_decreasing_brightness"
|
||||
SERVICE_STOP = "stop"
|
||||
|
||||
ENTITY_SERVICES = [
|
||||
SERVICE_START_INCREASING_BRIGHTNESS,
|
||||
SERVICE_START_DECREASING_BRIGHTNESS,
|
||||
SERVICE_STOP,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -48,14 +31,6 @@ async def async_setup_entry(
|
||||
data = entry.runtime_data
|
||||
hub = data.hub
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
for service in ENTITY_SERVICES:
|
||||
platform.async_register_entity_service(
|
||||
service,
|
||||
None,
|
||||
f"async_{service}",
|
||||
)
|
||||
|
||||
fan_lights: list[Entity] = [
|
||||
BondLight(data, device)
|
||||
for device in hub.devices
|
||||
@@ -94,22 +69,6 @@ async def async_setup_entry(
|
||||
if DeviceType.is_light(device.type)
|
||||
]
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
|
||||
{
|
||||
vol.Required(ATTR_BRIGHTNESS): vol.All(
|
||||
vol.Number(scale=0), vol.Range(0, 255)
|
||||
)
|
||||
},
|
||||
"async_set_brightness_belief",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
|
||||
{vol.Required(ATTR_POWER_STATE): vol.All(cv.boolean)},
|
||||
"async_set_power_belief",
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
fan_lights + fan_up_lights + fan_down_lights + fireplaces + fp_lights + lights,
|
||||
)
|
||||
|
||||
101
homeassistant/components/bond/services.py
Normal file
101
homeassistant/components/bond/services.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Support for Bond services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_POWER_STATE = "power_state"
|
||||
|
||||
# Fan
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state"
|
||||
|
||||
# Switch
|
||||
SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state"
|
||||
|
||||
# Light
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE = "set_light_power_tracked_state"
|
||||
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE = "set_light_brightness_tracked_state"
|
||||
SERVICE_START_INCREASING_BRIGHTNESS = "start_increasing_brightness"
|
||||
SERVICE_START_DECREASING_BRIGHTNESS = "start_decreasing_brightness"
|
||||
SERVICE_STOP = "stop"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
# Fan entity services
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
|
||||
entity_domain=FAN_DOMAIN,
|
||||
schema={vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))},
|
||||
func="async_set_speed_belief",
|
||||
)
|
||||
|
||||
# Light entity services
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_START_INCREASING_BRIGHTNESS,
|
||||
entity_domain=LIGHT_DOMAIN,
|
||||
schema=None,
|
||||
func="async_start_increasing_brightness",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_START_DECREASING_BRIGHTNESS,
|
||||
entity_domain=LIGHT_DOMAIN,
|
||||
schema=None,
|
||||
func="async_start_decreasing_brightness",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_STOP,
|
||||
entity_domain=LIGHT_DOMAIN,
|
||||
schema=None,
|
||||
func="async_stop",
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
|
||||
entity_domain=LIGHT_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_BRIGHTNESS): vol.All(
|
||||
vol.Number(scale=0), vol.Range(0, 255)
|
||||
)
|
||||
},
|
||||
func="async_set_brightness_belief",
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
|
||||
entity_domain=LIGHT_DOMAIN,
|
||||
schema={vol.Required(ATTR_POWER_STATE): vol.All(cv.boolean)},
|
||||
func="async_set_power_belief",
|
||||
)
|
||||
|
||||
# Switch entity services
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_POWER_TRACKED_STATE,
|
||||
entity_domain=SWITCH_DOMAIN,
|
||||
schema={vol.Required(ATTR_POWER_STATE): cv.boolean},
|
||||
func="async_set_power_belief",
|
||||
)
|
||||
@@ -6,16 +6,13 @@ from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from bond_async import Action, DeviceType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BondConfigEntry
|
||||
from .const import ATTR_POWER_STATE, SERVICE_SET_POWER_TRACKED_STATE
|
||||
from .entity import BondEntity
|
||||
|
||||
|
||||
@@ -26,12 +23,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond generic devices."""
|
||||
data = entry.runtime_data
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_POWER_TRACKED_STATE,
|
||||
{vol.Required(ATTR_POWER_STATE): cv.boolean},
|
||||
"async_set_power_belief",
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
BondSwitch(data, device)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.bluetooth import (
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
@@ -36,6 +36,45 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SE
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_encryption_issue_id(entry_id: str) -> str:
|
||||
"""Return the repair issue id for encryption removal."""
|
||||
return f"encryption_removed_{entry_id}"
|
||||
|
||||
|
||||
def _async_create_encryption_downgrade_issue(
|
||||
hass: HomeAssistant, entry: BTHomeConfigEntry, issue_id: str
|
||||
) -> None:
|
||||
"""Create a repair issue for encryption downgrade."""
|
||||
_LOGGER.warning(
|
||||
"BTHome device %s was previously encrypted but is now sending "
|
||||
"unencrypted data. This could be a spoofing attempt. "
|
||||
"Data will be ignored until resolved",
|
||||
entry.title,
|
||||
)
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="encryption_removed",
|
||||
translation_placeholders={"name": entry.title},
|
||||
data={"entry_id": entry.entry_id},
|
||||
)
|
||||
|
||||
|
||||
def _async_clear_encryption_downgrade_issue(
|
||||
hass: HomeAssistant, entry: BTHomeConfigEntry, issue_id: str
|
||||
) -> None:
|
||||
"""Clear the encryption downgrade repair issue."""
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
_LOGGER.info(
|
||||
"BTHome device %s is now sending encrypted data again. Resuming normal operation",
|
||||
entry.title,
|
||||
)
|
||||
|
||||
|
||||
def process_service_info(
|
||||
hass: HomeAssistant,
|
||||
entry: BTHomeConfigEntry,
|
||||
@@ -45,7 +84,26 @@ def process_service_info(
|
||||
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
|
||||
coordinator = entry.runtime_data
|
||||
data = coordinator.device_data
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue_id = get_encryption_issue_id(entry.entry_id)
|
||||
update = data.update(service_info)
|
||||
|
||||
# Block unencrypted payloads for devices that were previously verified as encrypted.
|
||||
if entry.data.get(CONF_BINDKEY) and data.downgrade_detected:
|
||||
if not coordinator.encryption_downgrade_logged:
|
||||
coordinator.encryption_downgrade_logged = True
|
||||
if not issue_registry.async_get_issue(DOMAIN, issue_id):
|
||||
_async_create_encryption_downgrade_issue(hass, entry, issue_id)
|
||||
return SensorUpdate(title=None, devices={})
|
||||
|
||||
if data.bindkey_verified and (
|
||||
(existing_issue := issue_registry.async_get_issue(DOMAIN, issue_id))
|
||||
or coordinator.encryption_downgrade_logged
|
||||
):
|
||||
coordinator.encryption_downgrade_logged = False
|
||||
if existing_issue:
|
||||
_async_clear_encryption_downgrade_issue(hass, entry, issue_id)
|
||||
|
||||
discovered_event_classes = coordinator.discovered_event_classes
|
||||
if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device:
|
||||
hass.config_entries.async_update_entry(
|
||||
@@ -150,3 +208,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> bo
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
ir.async_delete_issue(hass, DOMAIN, get_encryption_issue_id(entry.entry_id))
|
||||
|
||||
@@ -41,6 +41,8 @@ class BTHomePassiveBluetoothProcessorCoordinator(
|
||||
self.discovered_event_classes = discovered_event_classes
|
||||
self.device_data = device_data
|
||||
self.entry = entry
|
||||
# Track whether we've already logged the encryption downgrade this session.
|
||||
self.encryption_downgrade_logged = False
|
||||
|
||||
@property
|
||||
def sleepy_device(self) -> bool:
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.16.0"]
|
||||
"requirements": ["bthome-ble==3.17.0"]
|
||||
}
|
||||
|
||||
65
homeassistant/components/bthome/repairs.py
Normal file
65
homeassistant/components/bthome/repairs.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Repairs for the BTHome integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from . import get_encryption_issue_id
|
||||
from .const import CONF_BINDKEY, DOMAIN
|
||||
|
||||
|
||||
class EncryptionRemovedRepairFlow(RepairsFlow):
|
||||
"""Handle the repair flow when encryption is disabled."""
|
||||
|
||||
def __init__(self, entry_id: str, entry_title: str) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
self._entry_id = entry_id
|
||||
self._entry_title = entry_title
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the initial step of the repair flow."""
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle confirmation, remove the bindkey, and reload the entry."""
|
||||
if user_input is not None:
|
||||
entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||
if not entry:
|
||||
return self.async_abort(reason="entry_removed")
|
||||
|
||||
new_data = {k: v for k, v in entry.data.items() if k != CONF_BINDKEY}
|
||||
self.hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
ir.async_delete_issue(
|
||||
self.hass, DOMAIN, get_encryption_issue_id(self._entry_id)
|
||||
)
|
||||
|
||||
await self.hass.config_entries.async_reload(self._entry_id)
|
||||
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
description_placeholders={"name": self._entry_title},
|
||||
)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant, issue_id: str, data: dict[str, Any] | None
|
||||
) -> RepairsFlow:
|
||||
"""Create the repair flow for removing the encryption key."""
|
||||
if not data or "entry_id" not in data:
|
||||
raise ValueError("Missing data for repair flow")
|
||||
entry_id = data["entry_id"]
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
entry_title = entry.title if entry else "Unknown device"
|
||||
return EncryptionRemovedRepairFlow(entry_id, entry_title)
|
||||
@@ -117,5 +117,21 @@
|
||||
"name": "UV Index"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"encryption_removed": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"entry_removed": "The device has been removed"
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The BTHome device **{name}** was configured with encryption but is now broadcasting unencrypted data. Data from this device is being ignored until this is resolved.\n\nIf you disabled encryption on the device, select **Submit** to remove the encryption key and resume receiving data.\n\nIf you did not disable encryption, someone may be attempting to spoof your device. Do not submit this form and the unencrypted data will continue to be ignored.",
|
||||
"title": "Remove encryption key for {name}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Encryption disabled on {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +506,8 @@ def is_offset_reached(
|
||||
class CalendarEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""A class that describes calendar entities."""
|
||||
|
||||
initial_color: str | None = None
|
||||
|
||||
|
||||
class CalendarEntity(Entity):
|
||||
"""Base class for calendar event entities."""
|
||||
@@ -516,12 +518,16 @@ class CalendarEntity(Entity):
|
||||
|
||||
_alarm_unsubs: list[CALLBACK_TYPE] | None = None
|
||||
|
||||
_attr_initial_color: str | None = None
|
||||
_attr_initial_color: str | None
|
||||
|
||||
@property
|
||||
def initial_color(self) -> str | None:
|
||||
"""Return the initial color for the calendar entity."""
|
||||
return self._attr_initial_color
|
||||
if hasattr(self, "_attr_initial_color"):
|
||||
return self._attr_initial_color
|
||||
if hasattr(self, "entity_description"):
|
||||
return self.entity_description.initial_color
|
||||
return None
|
||||
|
||||
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
|
||||
"""Return initial entity options."""
|
||||
|
||||
@@ -50,11 +50,11 @@
|
||||
"selector": {},
|
||||
"services": {
|
||||
"disable_motion_detection": {
|
||||
"description": "Disables the motion detection.",
|
||||
"description": "Disables the motion detection of a camera.",
|
||||
"name": "Disable motion detection"
|
||||
},
|
||||
"enable_motion_detection": {
|
||||
"description": "Enables the motion detection.",
|
||||
"description": "Enables the motion detection of a camera.",
|
||||
"name": "Enable motion detection"
|
||||
},
|
||||
"play_stream": {
|
||||
@@ -100,11 +100,11 @@
|
||||
"name": "Take snapshot"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off the camera.",
|
||||
"description": "Turns off a camera.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on the camera.",
|
||||
"description": "Turns on a camera.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -260,9 +260,9 @@
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
|
||||
@@ -21,7 +21,7 @@ async def fetch_latest_carbon_intensity(
|
||||
em: ElectricityMaps,
|
||||
config: Mapping[str, Any],
|
||||
) -> HomeAssistantCarbonIntensityResponse:
|
||||
"""Fetch the latest carbon intensity based on country code or location coordinates."""
|
||||
"""Fetch the latest carbon intensity based on zone key or location coordinates."""
|
||||
request: CoordinatesRequest | ZoneRequest = CoordinatesRequest(
|
||||
lat=config.get(CONF_LATITUDE, hass.config.latitude),
|
||||
lon=config.get(CONF_LONGITUDE, hass.config.longitude),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"no_data": "No data is available for the location you have selected.",
|
||||
"no_data": "No data is available for the location or zone you have selected.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
@@ -17,20 +17,20 @@
|
||||
},
|
||||
"country": {
|
||||
"data": {
|
||||
"country_code": "Country code"
|
||||
"country_code": "Zone key"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::access_token%]"
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::access_token%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
},
|
||||
"description": "Visit the [Electricity Maps page]({register_link}) to request a token."
|
||||
"description": "Visit the [Electricity Maps app]({register_link}) to request an API key."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -40,7 +40,7 @@
|
||||
"name": "CO2 intensity",
|
||||
"state_attributes": {
|
||||
"country_code": {
|
||||
"name": "Country code"
|
||||
"name": "Zone key"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58,7 +58,7 @@
|
||||
"location": {
|
||||
"options": {
|
||||
"specify_coordinates": "Specify coordinates",
|
||||
"specify_country_code": "Specify country code",
|
||||
"specify_country_code": "Specify zone key",
|
||||
"use_home_location": "Use home location"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["compit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["compit-inext-api==0.6.0"]
|
||||
"requirements": ["compit-inext-api==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -58,12 +58,13 @@ C4_TO_HA_HVAC_MODE = {
|
||||
|
||||
HA_TO_C4_HVAC_MODE = {v: k for k, v in C4_TO_HA_HVAC_MODE.items()}
|
||||
|
||||
# Map Control4 HVAC state to Home Assistant HVAC action
|
||||
# Map the five known Control4 HVAC states to Home Assistant HVAC actions
|
||||
C4_TO_HA_HVAC_ACTION = {
|
||||
"heating": HVACAction.HEATING,
|
||||
"cooling": HVACAction.COOLING,
|
||||
"idle": HVACAction.IDLE,
|
||||
"off": HVACAction.OFF,
|
||||
"heat": HVACAction.HEATING,
|
||||
"cool": HVACAction.COOLING,
|
||||
"dry": HVACAction.DRYING,
|
||||
"fan": HVACAction.FAN,
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +237,10 @@ class Control4Climate(Control4Entity, ClimateEntity):
|
||||
if c4_state is None:
|
||||
return None
|
||||
# Convert state to lowercase for mapping
|
||||
return C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower())
|
||||
action = C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower())
|
||||
if action is None:
|
||||
_LOGGER.debug("Unknown HVAC state received from Control4: %s", c4_state)
|
||||
return action
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
|
||||
@@ -335,20 +335,18 @@ def _get_config_intents(config: ConfigType, hass_config_path: str) -> dict[str,
|
||||
"""Return config intents."""
|
||||
intents = config.get(DOMAIN, {}).get("intents", {})
|
||||
return {
|
||||
"intents": {
|
||||
intent_name: {
|
||||
"data": [
|
||||
{
|
||||
"sentences": sentences,
|
||||
"metadata": {
|
||||
METADATA_CUSTOM_SENTENCE: True,
|
||||
METADATA_CUSTOM_FILE: hass_config_path,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
for intent_name, sentences in intents.items()
|
||||
intent_name: {
|
||||
"data": [
|
||||
{
|
||||
"sentences": sentences,
|
||||
"metadata": {
|
||||
METADATA_CUSTOM_SENTENCE: True,
|
||||
METADATA_CUSTOM_FILE: hass_config_path,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
for intent_name, sentences in intents.items()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -18,7 +19,7 @@ from homeassistant.core import (
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, intent, singleton
|
||||
|
||||
from .const import DATA_COMPONENT, HOME_ASSISTANT_AGENT
|
||||
from .const import DATA_COMPONENT, HOME_ASSISTANT_AGENT, IntentSource
|
||||
from .entity import ConversationEntity
|
||||
from .models import (
|
||||
AbstractConversationAgent,
|
||||
@@ -34,9 +35,11 @@ from .trace import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_INTENT_NAME_PREFIX = "HassSentenceTrigger"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .default_agent import DefaultAgent
|
||||
from .trigger import TriggerDetails
|
||||
from .trigger import TRIGGER_CALLBACK_TYPE
|
||||
|
||||
|
||||
@singleton.singleton("conversation_agent")
|
||||
@@ -139,6 +142,10 @@ async def async_converse(
|
||||
return result
|
||||
|
||||
|
||||
type IntentSourceConfig = dict[str, dict[str, Any]]
|
||||
type IntentsCallback = Callable[[dict[IntentSource, IntentSourceConfig]], None]
|
||||
|
||||
|
||||
class AgentManager:
|
||||
"""Class to manage conversation agents."""
|
||||
|
||||
@@ -147,8 +154,13 @@ class AgentManager:
|
||||
self.hass = hass
|
||||
self._agents: dict[str, AbstractConversationAgent] = {}
|
||||
self.default_agent: DefaultAgent | None = None
|
||||
self.config_intents: dict[str, Any] = {}
|
||||
self.triggers_details: list[TriggerDetails] = []
|
||||
self._intents: dict[IntentSource, IntentSourceConfig] = {
|
||||
IntentSource.CONFIG: {"intents": {}},
|
||||
IntentSource.TRIGGER: {"intents": {}},
|
||||
}
|
||||
self._intents_subscribers: list[IntentsCallback] = []
|
||||
self._trigger_callbacks: dict[int, TRIGGER_CALLBACK_TYPE] = {}
|
||||
self._trigger_callback_counter: int = 0
|
||||
|
||||
@callback
|
||||
def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None:
|
||||
@@ -200,27 +212,75 @@ class AgentManager:
|
||||
|
||||
async def async_setup_default_agent(self, agent: DefaultAgent) -> None:
|
||||
"""Set up the default agent."""
|
||||
agent.update_config_intents(self.config_intents)
|
||||
agent.update_triggers(self.triggers_details)
|
||||
self.default_agent = agent
|
||||
|
||||
@callback
|
||||
def subscribe_intents(self, subscriber: IntentsCallback) -> CALLBACK_TYPE:
|
||||
"""Subscribe to intents updates.
|
||||
|
||||
The subscriber callback is called immediately with all intent sources
|
||||
and whenever intents are updated (only with the changed source).
|
||||
"""
|
||||
subscriber(self._intents)
|
||||
self._intents_subscribers.append(subscriber)
|
||||
|
||||
@callback
|
||||
def unsubscribe() -> None:
|
||||
"""Unsubscribe from intents updates."""
|
||||
self._intents_subscribers.remove(subscriber)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def _notify_intents_subscribers(self, source: IntentSource) -> None:
|
||||
"""Notify all intents subscribers of a change to a specific source."""
|
||||
update = {source: self._intents[source]}
|
||||
for subscriber in self._intents_subscribers:
|
||||
subscriber(update)
|
||||
|
||||
def update_config_intents(self, intents: dict[str, Any]) -> None:
|
||||
"""Update config intents."""
|
||||
self.config_intents = intents
|
||||
if self.default_agent is not None:
|
||||
self.default_agent.update_config_intents(intents)
|
||||
self._intents[IntentSource.CONFIG]["intents"] = intents
|
||||
self._notify_intents_subscribers(IntentSource.CONFIG)
|
||||
|
||||
def register_trigger(self, trigger_details: TriggerDetails) -> CALLBACK_TYPE:
|
||||
def register_trigger(
|
||||
self, sentences: list[str], trigger_callback: TRIGGER_CALLBACK_TYPE
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register a trigger."""
|
||||
self.triggers_details.append(trigger_details)
|
||||
if self.default_agent is not None:
|
||||
self.default_agent.update_triggers(self.triggers_details)
|
||||
trigger_id = self._trigger_callback_counter
|
||||
self._trigger_callback_counter += 1
|
||||
trigger_intent_name = f"{TRIGGER_INTENT_NAME_PREFIX}{trigger_id}"
|
||||
|
||||
trigger_intents = self._intents[IntentSource.TRIGGER]
|
||||
trigger_intents["intents"][trigger_intent_name] = {
|
||||
"data": [{"sentences": sentences}]
|
||||
}
|
||||
self._trigger_callbacks[trigger_id] = trigger_callback
|
||||
self._notify_intents_subscribers(IntentSource.TRIGGER)
|
||||
|
||||
@callback
|
||||
def unregister_trigger() -> None:
|
||||
"""Unregister the trigger."""
|
||||
self.triggers_details.remove(trigger_details)
|
||||
if self.default_agent is not None:
|
||||
self.default_agent.update_triggers(self.triggers_details)
|
||||
del trigger_intents["intents"][trigger_intent_name]
|
||||
del self._trigger_callbacks[trigger_id]
|
||||
self._notify_intents_subscribers(IntentSource.TRIGGER)
|
||||
|
||||
return unregister_trigger
|
||||
|
||||
@property
|
||||
def trigger_sentences(self) -> list[str]:
|
||||
"""Get all trigger sentences."""
|
||||
sentences: list[str] = []
|
||||
trigger_intents = self._intents[IntentSource.TRIGGER]
|
||||
for trigger_intent in trigger_intents.get("intents", {}).values():
|
||||
for data in trigger_intent.get("data", []):
|
||||
sentences.extend(data.get("sentences", []))
|
||||
return sentences
|
||||
|
||||
def get_trigger_callback(
|
||||
self, trigger_intent_name: str
|
||||
) -> TRIGGER_CALLBACK_TYPE | None:
|
||||
"""Get the callback for a trigger from its intent name."""
|
||||
if not trigger_intent_name.startswith(TRIGGER_INTENT_NAME_PREFIX):
|
||||
return None
|
||||
trigger_id = int(trigger_intent_name[len(TRIGGER_INTENT_NAME_PREFIX) :])
|
||||
return self._trigger_callbacks.get(trigger_id)
|
||||
|
||||
@@ -36,6 +36,13 @@ METADATA_CUSTOM_SENTENCE = "hass_custom_sentence"
|
||||
METADATA_CUSTOM_FILE = "hass_custom_file"
|
||||
|
||||
|
||||
class IntentSource(StrEnum):
|
||||
"""Source of intents."""
|
||||
|
||||
CONFIG = "config"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
|
||||
class ChatLogEventType(StrEnum):
|
||||
"""Chat log event type."""
|
||||
|
||||
|
||||
@@ -76,18 +76,18 @@ from homeassistant.helpers.event import async_track_state_added_domain
|
||||
from homeassistant.util import language as language_util
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .agent_manager import get_agent_manager
|
||||
from .agent_manager import IntentSourceConfig, get_agent_manager
|
||||
from .chat_log import AssistantContent, ChatLog, ToolResultContent
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
METADATA_CUSTOM_FILE,
|
||||
METADATA_CUSTOM_SENTENCE,
|
||||
ConversationEntityFeature,
|
||||
IntentSource,
|
||||
)
|
||||
from .entity import ConversationEntity
|
||||
from .models import ConversationInput, ConversationResult
|
||||
from .trace import ConversationTraceEventType, async_conversation_trace_append
|
||||
from .trigger import TriggerDetails
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -126,7 +126,7 @@ class SentenceTriggerResult:
|
||||
|
||||
sentence: str
|
||||
sentence_template: str | None
|
||||
matched_triggers: dict[int, RecognizeResult]
|
||||
matched_triggers: dict[str, RecognizeResult]
|
||||
|
||||
|
||||
class IntentMatchingStage(Enum):
|
||||
@@ -236,15 +236,19 @@ class DefaultAgent(ConversationEntity):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the default agent."""
|
||||
self.hass = hass
|
||||
|
||||
self._lang_intents: dict[str, LanguageIntents | object] = {}
|
||||
self._load_intents_lock = asyncio.Lock()
|
||||
|
||||
# Intents from common conversation config
|
||||
self._config_intents: dict[str, Any] = {}
|
||||
self._config_intents_config: IntentSourceConfig = {}
|
||||
|
||||
# Sentences that will trigger a callback (skipping intent recognition)
|
||||
self._triggers_details: list[TriggerDetails] = []
|
||||
# Intents from conversation triggers
|
||||
self._trigger_intents: Intents | None = None
|
||||
self._trigger_intents_config: IntentSourceConfig = {}
|
||||
|
||||
# Subscription to intents updates
|
||||
self._unsub_intents: Callable[[], None] | None = None
|
||||
|
||||
# Slot lists for entities, areas, etc.
|
||||
self._slot_lists: dict[str, SlotList] | None = None
|
||||
@@ -261,6 +265,33 @@ class DefaultAgent(ConversationEntity):
|
||||
self.fuzzy_matching = True
|
||||
self._fuzzy_config: FuzzyConfig | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to intents updates when added to hass."""
|
||||
self._unsub_intents = get_agent_manager(self.hass).subscribe_intents(
|
||||
self._update_intents
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from intents updates when removed from hass."""
|
||||
if self._unsub_intents is not None:
|
||||
self._unsub_intents()
|
||||
self._unsub_intents = None
|
||||
|
||||
@callback
|
||||
def _update_intents(
|
||||
self, intents_update: dict[IntentSource, IntentSourceConfig]
|
||||
) -> None:
|
||||
"""Handle intents update from agent_manager subscription."""
|
||||
if IntentSource.CONFIG in intents_update:
|
||||
self._config_intents_config = intents_update[IntentSource.CONFIG]
|
||||
# Intents have changed, so we must clear the cache
|
||||
self._intent_cache.clear()
|
||||
|
||||
if IntentSource.TRIGGER in intents_update:
|
||||
self._trigger_intents_config = intents_update[IntentSource.TRIGGER]
|
||||
# Force rebuild on next use
|
||||
self._trigger_intents = None
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return a list of supported languages."""
|
||||
@@ -1059,14 +1090,6 @@ class DefaultAgent(ConversationEntity):
|
||||
# Intents have changed, so we must clear the cache
|
||||
self._intent_cache.clear()
|
||||
|
||||
@callback
|
||||
def update_config_intents(self, intents: dict[str, Any]) -> None:
|
||||
"""Update config intents."""
|
||||
self._config_intents = intents
|
||||
|
||||
# Intents have changed, so we must clear the cache
|
||||
self._intent_cache.clear()
|
||||
|
||||
async def async_prepare(self, language: str | None = None) -> None:
|
||||
"""Load intents for a language."""
|
||||
if language is None:
|
||||
@@ -1193,7 +1216,7 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
merge_dict(
|
||||
intents_dict,
|
||||
self._config_intents,
|
||||
self._config_intents_config,
|
||||
)
|
||||
|
||||
if not intents_dict:
|
||||
@@ -1461,27 +1484,12 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
return response_template.async_render(response_args)
|
||||
|
||||
@callback
|
||||
def update_triggers(self, triggers_details: list[TriggerDetails]) -> None:
|
||||
"""Update triggers."""
|
||||
self._triggers_details = triggers_details
|
||||
|
||||
# Force rebuild on next use
|
||||
self._trigger_intents = None
|
||||
|
||||
def _rebuild_trigger_intents(self) -> None:
|
||||
"""Rebuild the HassIL intents object from the current trigger sentences."""
|
||||
"""Rebuild the HassIL intents object from the trigger intents dict."""
|
||||
intents_dict = {
|
||||
"language": self.hass.config.language,
|
||||
"intents": {
|
||||
# Use trigger data index as a virtual intent name for HassIL.
|
||||
# This works because the intents are rebuilt on every
|
||||
# register/unregister.
|
||||
str(trigger_id): {"data": [{"sentences": trigger_details.sentences}]}
|
||||
for trigger_id, trigger_details in enumerate(self._triggers_details)
|
||||
},
|
||||
**self._trigger_intents_config,
|
||||
}
|
||||
|
||||
trigger_intents = Intents.from_dict(intents_dict)
|
||||
|
||||
# Assume slot list references are wildcards
|
||||
@@ -1496,7 +1504,7 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
self._trigger_intents = trigger_intents
|
||||
|
||||
_LOGGER.debug("Rebuilt trigger intents: %s", intents_dict)
|
||||
_LOGGER.debug("Rebuilt trigger intents: %s", self._trigger_intents_config)
|
||||
|
||||
async def async_recognize_sentence_trigger(
|
||||
self, user_input: ConversationInput
|
||||
@@ -1506,7 +1514,7 @@ class DefaultAgent(ConversationEntity):
|
||||
Calls the registered callbacks if there's a match and returns a sentence
|
||||
trigger result.
|
||||
"""
|
||||
if not self._triggers_details:
|
||||
if not self._trigger_intents_config.get("intents"):
|
||||
# No triggers registered
|
||||
return None
|
||||
|
||||
@@ -1516,18 +1524,18 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
assert self._trigger_intents is not None
|
||||
|
||||
matched_triggers: dict[int, RecognizeResult] = {}
|
||||
matched_triggers: dict[str, RecognizeResult] = {}
|
||||
matched_template: str | None = None
|
||||
for result in recognize_all(user_input.text, self._trigger_intents):
|
||||
if result.intent_sentence is not None:
|
||||
matched_template = result.intent_sentence.text
|
||||
|
||||
trigger_id = int(result.intent.name)
|
||||
if trigger_id in matched_triggers:
|
||||
trigger_intent_name = result.intent.name
|
||||
if trigger_intent_name in matched_triggers:
|
||||
# Already matched a sentence from this trigger
|
||||
break
|
||||
|
||||
matched_triggers[trigger_id] = result
|
||||
matched_triggers[trigger_intent_name] = result
|
||||
|
||||
if not matched_triggers:
|
||||
# Sentence did not match any trigger sentences
|
||||
@@ -1551,10 +1559,14 @@ class DefaultAgent(ConversationEntity):
|
||||
chat_log: ChatLog,
|
||||
) -> str:
|
||||
"""Run sentence trigger callbacks and return response text."""
|
||||
manager = get_agent_manager(self.hass)
|
||||
|
||||
# Gather callback responses in parallel
|
||||
trigger_callbacks = [
|
||||
self._triggers_details[trigger_id].callback(user_input, trigger_result)
|
||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||
trigger_callback(user_input, trigger_result)
|
||||
for trigger_intent_name, trigger_result in result.matched_triggers.items()
|
||||
if (trigger_callback := manager.get_trigger_callback(trigger_intent_name))
|
||||
is not None
|
||||
]
|
||||
|
||||
tool_input = llm.ToolInput(
|
||||
|
||||
@@ -165,11 +165,7 @@ async def websocket_list_sentences(
|
||||
"""List custom registered sentences."""
|
||||
manager = get_agent_manager(hass)
|
||||
|
||||
sentences = []
|
||||
for trigger_details in manager.triggers_details:
|
||||
sentences.extend(trigger_details.sentences)
|
||||
|
||||
connection.send_result(msg["id"], {"trigger_sentences": sentences})
|
||||
connection.send_result(msg["id"], {"trigger_sentences": manager.trigger_sentences})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from hassil.recognize import RecognizeResult
|
||||
@@ -31,14 +30,6 @@ TRIGGER_CALLBACK_TYPE = Callable[
|
||||
]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TriggerDetails:
|
||||
"""List of sentences and the callback for a trigger."""
|
||||
|
||||
sentences: list[str]
|
||||
callback: TRIGGER_CALLBACK_TYPE
|
||||
|
||||
|
||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
"""Validate result does not contain punctuation."""
|
||||
for sentence in value:
|
||||
@@ -149,5 +140,5 @@ async def async_attach_trigger(
|
||||
return None
|
||||
|
||||
return get_agent_manager(hass).register_trigger(
|
||||
TriggerDetails(sentences=sentences, callback=call_action)
|
||||
sentences=sentences, trigger_callback=call_action
|
||||
)
|
||||
|
||||
@@ -46,13 +46,12 @@ class DSMRSensor(SensorEntity):
|
||||
@callback
|
||||
def message_received(message):
|
||||
"""Handle new MQTT messages."""
|
||||
if message.payload == "":
|
||||
if not (payload := message.payload):
|
||||
self._attr_native_value = None
|
||||
elif self.entity_description.state is not None:
|
||||
# Perform optional additional parsing
|
||||
self._attr_native_value = self.entity_description.state(message.payload)
|
||||
elif (state := self.entity_description.state) is not None:
|
||||
self._attr_native_value = state(payload)
|
||||
else:
|
||||
self._attr_native_value = message.payload
|
||||
self._attr_native_value = payload
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["eheimdigital==1.5.0"],
|
||||
"requirements": ["eheimdigital==1.6.0"],
|
||||
"zeroconf": [
|
||||
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ElgatoConfigEntry, ElgatoDataUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.BUTTON, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ElgatoConfigEntry) -> bool:
|
||||
"""Set up Elgato Light from a config entry."""
|
||||
coordinator = ElgatoDataUpdateCoordinator(hass, entry)
|
||||
|
||||
@@ -14,6 +14,3 @@ SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
# Attributes
|
||||
ATTR_ON = "on"
|
||||
|
||||
# Services
|
||||
SERVICE_IDENTIFY = "identify"
|
||||
|
||||
@@ -15,13 +15,9 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .const import SERVICE_IDENTIFY
|
||||
from .coordinator import ElgatoConfigEntry, ElgatoDataUpdateCoordinator
|
||||
from .entity import ElgatoEntity
|
||||
|
||||
@@ -37,13 +33,6 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities([ElgatoLight(coordinator)])
|
||||
|
||||
platform = async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_IDENTIFY,
|
||||
None,
|
||||
ElgatoLight.async_identify.__name__,
|
||||
)
|
||||
|
||||
|
||||
class ElgatoLight(ElgatoEntity, LightEntity):
|
||||
"""Defines an Elgato Light."""
|
||||
|
||||
25
homeassistant/components/elgato/services.py
Normal file
25
homeassistant/components/elgato/services.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Support for Elgato services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_IDENTIFY = "identify"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_IDENTIFY,
|
||||
entity_domain=LIGHT_DOMAIN,
|
||||
schema=None,
|
||||
func="async_identify",
|
||||
)
|
||||
@@ -8,9 +8,11 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_ENOCEAN, DOMAIN, ENOCEAN_DONGLE
|
||||
from .const import DOMAIN
|
||||
from .dongle import EnOceanDongle
|
||||
|
||||
type EnOceanConfigEntry = ConfigEntry[EnOceanDongle]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
@@ -36,21 +38,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: EnOceanConfigEntry
|
||||
) -> bool:
|
||||
"""Set up an EnOcean dongle for the given entry."""
|
||||
enocean_data = hass.data.setdefault(DATA_ENOCEAN, {})
|
||||
usb_dongle = EnOceanDongle(hass, config_entry.data[CONF_DEVICE])
|
||||
await usb_dongle.async_setup()
|
||||
enocean_data[ENOCEAN_DONGLE] = usb_dongle
|
||||
config_entry.runtime_data = usb_dongle
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: EnOceanConfigEntry
|
||||
) -> bool:
|
||||
"""Unload EnOcean config entry."""
|
||||
|
||||
enocean_dongle = hass.data[DATA_ENOCEAN][ENOCEAN_DONGLE]
|
||||
enocean_dongle = config_entry.runtime_data
|
||||
enocean_dongle.unload()
|
||||
hass.data.pop(DATA_ENOCEAN)
|
||||
|
||||
return True
|
||||
|
||||
@@ -5,8 +5,6 @@ import logging
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "enocean"
|
||||
DATA_ENOCEAN = "enocean"
|
||||
ENOCEAN_DONGLE = "dongle"
|
||||
|
||||
ERROR_INVALID_DONGLE_PATH = "invalid_dongle_path"
|
||||
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ import asyncio.exceptions
|
||||
from typing import Any
|
||||
|
||||
from flexit_bacnet import (
|
||||
OPERATION_MODE_FIREPLACE,
|
||||
OPERATION_MODE_OFF,
|
||||
VENTILATION_MODE_AWAY,
|
||||
VENTILATION_MODE_HOME,
|
||||
VENTILATION_MODE_STOP,
|
||||
@@ -12,7 +14,6 @@ from flexit_bacnet.bacnet import DecodingError
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_HOME,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
@@ -28,8 +29,10 @@ from .const import (
|
||||
DOMAIN,
|
||||
MAX_TEMP,
|
||||
MIN_TEMP,
|
||||
OPERATION_TO_PRESET_MODE_MAP,
|
||||
PRESET_FIREPLACE,
|
||||
PRESET_HIGH,
|
||||
PRESET_TO_VENTILATION_MODE_MAP,
|
||||
VENTILATION_TO_PRESET_MODE_MAP,
|
||||
)
|
||||
from .coordinator import FlexitConfigEntry, FlexitCoordinator
|
||||
from .entity import FlexitEntity
|
||||
@@ -51,6 +54,7 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
"""Flexit air handling unit."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_translation_key = "flexit_bacnet"
|
||||
|
||||
_attr_hvac_modes = [
|
||||
HVACMode.OFF,
|
||||
@@ -60,7 +64,8 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
_attr_preset_modes = [
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
PRESET_BOOST,
|
||||
PRESET_HIGH,
|
||||
PRESET_FIREPLACE,
|
||||
]
|
||||
|
||||
_attr_supported_features = (
|
||||
@@ -127,20 +132,29 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
|
||||
Requires ClimateEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
return VENTILATION_TO_PRESET_MODE_MAP[self.device.ventilation_mode]
|
||||
return OPERATION_TO_PRESET_MODE_MAP[self.device.operation_mode]
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
|
||||
|
||||
try:
|
||||
await self.device.set_ventilation_mode(ventilation_mode)
|
||||
if preset_mode == PRESET_FIREPLACE:
|
||||
# Use trigger method for fireplace mode
|
||||
await self.device.trigger_fireplace_mode()
|
||||
else:
|
||||
# If currently in fireplace mode, toggle it off first
|
||||
# trigger_fireplace_mode() acts as a toggle
|
||||
if self.device.operation_mode == OPERATION_MODE_FIREPLACE:
|
||||
await self.device.trigger_fireplace_mode()
|
||||
|
||||
# Set the desired ventilation mode
|
||||
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
|
||||
await self.device.set_ventilation_mode(ventilation_mode)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_preset_mode",
|
||||
translation_placeholders={
|
||||
"preset": str(ventilation_mode),
|
||||
"preset": preset_mode,
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
@@ -149,7 +163,7 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
if self.device.ventilation_mode == VENTILATION_MODE_STOP:
|
||||
if self.device.operation_mode == OPERATION_MODE_OFF:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.FAN_ONLY
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
"""Constants for the Flexit Nordic (BACnet) integration."""
|
||||
|
||||
from flexit_bacnet import (
|
||||
OPERATION_MODE_AWAY,
|
||||
OPERATION_MODE_FIREPLACE,
|
||||
OPERATION_MODE_HIGH,
|
||||
OPERATION_MODE_HOME,
|
||||
OPERATION_MODE_OFF,
|
||||
VENTILATION_MODE_AWAY,
|
||||
VENTILATION_MODE_HIGH,
|
||||
VENTILATION_MODE_HOME,
|
||||
VENTILATION_MODE_STOP,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
)
|
||||
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME, PRESET_NONE
|
||||
|
||||
DOMAIN = "flexit_bacnet"
|
||||
|
||||
MAX_TEMP = 30
|
||||
MIN_TEMP = 10
|
||||
|
||||
VENTILATION_TO_PRESET_MODE_MAP = {
|
||||
VENTILATION_MODE_STOP: PRESET_NONE,
|
||||
VENTILATION_MODE_AWAY: PRESET_AWAY,
|
||||
VENTILATION_MODE_HOME: PRESET_HOME,
|
||||
VENTILATION_MODE_HIGH: PRESET_BOOST,
|
||||
PRESET_HIGH = "high"
|
||||
PRESET_FIREPLACE = "fireplace"
|
||||
|
||||
# Map operation mode (what device reports) to Home Assistant preset
|
||||
OPERATION_TO_PRESET_MODE_MAP = {
|
||||
OPERATION_MODE_OFF: PRESET_NONE,
|
||||
OPERATION_MODE_AWAY: PRESET_AWAY,
|
||||
OPERATION_MODE_HOME: PRESET_HOME,
|
||||
OPERATION_MODE_HIGH: PRESET_HIGH,
|
||||
OPERATION_MODE_FIREPLACE: PRESET_FIREPLACE,
|
||||
}
|
||||
|
||||
# Map preset to ventilation mode (for setting standard modes)
|
||||
PRESET_TO_VENTILATION_MODE_MAP = {
|
||||
PRESET_NONE: VENTILATION_MODE_STOP,
|
||||
PRESET_AWAY: VENTILATION_MODE_AWAY,
|
||||
PRESET_HOME: VENTILATION_MODE_HOME,
|
||||
PRESET_BOOST: VENTILATION_MODE_HIGH,
|
||||
PRESET_HIGH: VENTILATION_MODE_HIGH,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"flexit_bacnet": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"fireplace": "mdi:fireplace",
|
||||
"high": "mdi:fan-speed-3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"away_extract_fan_setpoint": {
|
||||
"default": "mdi:fan-minus"
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
"name": "Air filter polluted"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"flexit_bacnet": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"fireplace": "Fireplace",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"away_extract_fan_setpoint": {
|
||||
"name": "Away extract fan setpoint"
|
||||
@@ -139,5 +151,11 @@
|
||||
"switch_turn": {
|
||||
"message": "Failed to turn the switch {state}."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_fireplace_switch": {
|
||||
"description": "The fireplace mode switch entity `{entity_id}` is deprecated and will be removed in a future version.\n\nFireplace mode has been moved to a climate preset on the climate entity to better match the device interface.\n\nPlease update your automations to use the `climate.set_preset_mode` action with preset mode `fireplace` instead of using the switch entity.\n\nAfter updating your automations, you can safely disable this switch entity.",
|
||||
"title": "Fireplace mode switch is deprecated"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FlexitConfigEntry, FlexitCoordinator
|
||||
@@ -39,13 +42,6 @@ SWITCHES: tuple[FlexitSwitchEntityDescription, ...] = (
|
||||
turn_on_fn=lambda data: data.enable_electric_heater(),
|
||||
turn_off_fn=lambda data: data.disable_electric_heater(),
|
||||
),
|
||||
FlexitSwitchEntityDescription(
|
||||
key="fireplace_mode",
|
||||
translation_key="fireplace_mode",
|
||||
is_on_fn=lambda data: data.fireplace_ventilation_status,
|
||||
turn_on_fn=lambda data: data.trigger_fireplace_mode(),
|
||||
turn_off_fn=lambda data: data.trigger_fireplace_mode(),
|
||||
),
|
||||
FlexitSwitchEntityDescription(
|
||||
key="cooker_hood_mode",
|
||||
translation_key="cooker_hood_mode",
|
||||
@@ -53,6 +49,13 @@ SWITCHES: tuple[FlexitSwitchEntityDescription, ...] = (
|
||||
turn_on_fn=lambda data: data.activate_cooker_hood(),
|
||||
turn_off_fn=lambda data: data.deactivate_cooker_hood(),
|
||||
),
|
||||
FlexitSwitchEntityDescription(
|
||||
key="fireplace_mode",
|
||||
translation_key="fireplace_mode",
|
||||
is_on_fn=lambda data: data.fireplace_ventilation_status,
|
||||
turn_on_fn=lambda data: data.trigger_fireplace_mode(),
|
||||
turn_off_fn=lambda data: data.trigger_fireplace_mode(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -64,9 +67,42 @@ async def async_setup_entry(
|
||||
"""Set up Flexit (bacnet) switch from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
FlexitSwitch(coordinator, description) for description in SWITCHES
|
||||
)
|
||||
entities: list[FlexitSwitch] = []
|
||||
for description in SWITCHES:
|
||||
if description.key == "fireplace_mode":
|
||||
# Check if deprecated fireplace switch is enabled and create repair issue
|
||||
entity_reg = er.async_get(hass)
|
||||
fireplace_switch_unique_id = (
|
||||
f"{coordinator.device.serial_number}-fireplace_mode"
|
||||
)
|
||||
# Look up the fireplace switch entity by unique_id
|
||||
fireplace_switch_entity_id = entity_reg.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, fireplace_switch_unique_id
|
||||
)
|
||||
if not fireplace_switch_entity_id:
|
||||
continue
|
||||
entity_registry_entry = entity_reg.async_get(fireplace_switch_entity_id)
|
||||
|
||||
if entity_registry_entry:
|
||||
if entity_registry_entry.disabled:
|
||||
entity_reg.async_remove(fireplace_switch_entity_id)
|
||||
else:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_switch_{fireplace_switch_unique_id}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_fireplace_switch",
|
||||
translation_placeholders={
|
||||
"entity_id": fireplace_switch_entity_id,
|
||||
},
|
||||
)
|
||||
entities.append(FlexitSwitch(coordinator, description))
|
||||
else:
|
||||
entities.append(FlexitSwitch(coordinator, description))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["fressnapftracker==0.2.1"]
|
||||
"requirements": ["fressnapftracker==0.2.2"]
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"preview_features": {
|
||||
"winter_mode": {}
|
||||
},
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260128.1"]
|
||||
"requirements": ["home-assistant-frontend==20260128.4"]
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ from homeassistant.helpers import (
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.device import async_entity_id_to_device_id
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import (
|
||||
async_handle_source_entity_changes,
|
||||
@@ -96,13 +93,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
|
||||
# This can be removed in HA Core 2026.2
|
||||
async_remove_stale_devices_links_keep_entity_device(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
entry.options[CONF_HUMIDIFIER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
|
||||
@@ -5,10 +5,7 @@ import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.device import async_entity_id_to_device_id
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import (
|
||||
async_handle_source_entity_changes,
|
||||
@@ -23,13 +20,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
|
||||
# This can be removed in HA Core 2026.2
|
||||
async_remove_stale_devices_links_keep_entity_device(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
entry.options[CONF_HEATER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
|
||||
@@ -101,7 +101,6 @@ class GoogleCalendarEntityDescription(CalendarEntityDescription):
|
||||
search: str | None
|
||||
local_sync: bool
|
||||
device_id: str
|
||||
initial_color: str | None = None
|
||||
event_type: EventTypeEnum | None = None
|
||||
|
||||
|
||||
@@ -361,7 +360,6 @@ class GoogleCalendarEntity(
|
||||
if entity_description.entity_id:
|
||||
self.entity_id = entity_description.entity_id
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_initial_color = entity_description.initial_color
|
||||
if not entity_description.read_only:
|
||||
self._attr_supported_features = (
|
||||
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.2"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.3"]
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return battery value of the device."""
|
||||
return self._battery
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from .const import (
|
||||
)
|
||||
from .coordinator import GrowattConfigEntry, GrowattCoordinator
|
||||
from .models import GrowattRuntimeData
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +37,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Growatt Server component."""
|
||||
# Register services
|
||||
await async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
@@ -21,7 +21,8 @@ if TYPE_CHECKING:
|
||||
from .coordinator import GrowattCoordinator
|
||||
|
||||
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for Growatt Server integration."""
|
||||
|
||||
def get_min_coordinators() -> dict[str, GrowattCoordinator]:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyHik==0.4.1"]
|
||||
"requirements": ["pyHik==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.device import async_entity_id_to_device_id
|
||||
from homeassistant.helpers.helper_integration import (
|
||||
async_handle_source_entity_changes,
|
||||
async_remove_helper_config_entry_from_source_device,
|
||||
@@ -53,13 +50,6 @@ async def async_setup_entry(
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# This can be removed in HA Core 2026.2
|
||||
async_remove_stale_devices_links_keep_entity_device(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
|
||||
@@ -169,6 +169,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect binary sensor."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -73,6 +73,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect button entities."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -7,18 +7,44 @@ from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
|
||||
|
||||
def should_add_option_entity(
|
||||
description: EntityDescription,
|
||||
appliance: HomeConnectApplianceData,
|
||||
entity_registry: er.EntityRegistry,
|
||||
platform: Platform,
|
||||
) -> bool:
|
||||
"""Check if the option entity should be added for the appliance.
|
||||
|
||||
This function returns `True` if the option is available in the appliance options
|
||||
or if the entity was added in previous loads of this integration.
|
||||
"""
|
||||
description_key = description.key
|
||||
return description_key in appliance.options or (
|
||||
entity_registry.async_get_entity_id(
|
||||
platform, DOMAIN, f"{appliance.info.ha_id}-{description_key}"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def _create_option_entities(
|
||||
entity_registry: er.EntityRegistry,
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
known_entity_unique_ids: dict[str, str],
|
||||
get_option_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData],
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData, er.EntityRegistry],
|
||||
list[HomeConnectOptionEntity],
|
||||
],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
@@ -26,7 +52,9 @@ def _create_option_entities(
|
||||
"""Create the required option entities for the appliances."""
|
||||
option_entities_to_add = [
|
||||
entity
|
||||
for entity in get_option_entities_for_appliance(entry, appliance)
|
||||
for entity in get_option_entities_for_appliance(
|
||||
entry, appliance, entity_registry
|
||||
)
|
||||
if entity.unique_id not in known_entity_unique_ids
|
||||
]
|
||||
known_entity_unique_ids.update(
|
||||
@@ -39,13 +67,14 @@ def _create_option_entities(
|
||||
|
||||
|
||||
def _handle_paired_or_connected_appliance(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
known_entity_unique_ids: dict[str, str],
|
||||
get_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData], list[HomeConnectEntity]
|
||||
],
|
||||
get_option_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData],
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData, er.EntityRegistry],
|
||||
list[HomeConnectOptionEntity],
|
||||
]
|
||||
| None,
|
||||
@@ -60,6 +89,7 @@ def _handle_paired_or_connected_appliance(
|
||||
already or it is the first time we see them when the appliance is connected.
|
||||
"""
|
||||
entities: list[HomeConnectEntity] = []
|
||||
entity_registry = er.async_get(hass)
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
entities_to_add = [
|
||||
entity
|
||||
@@ -69,7 +99,9 @@ def _handle_paired_or_connected_appliance(
|
||||
if get_option_entities_for_appliance:
|
||||
entities_to_add.extend(
|
||||
entity
|
||||
for entity in get_option_entities_for_appliance(entry, appliance)
|
||||
for entity in get_option_entities_for_appliance(
|
||||
entry, appliance, entity_registry
|
||||
)
|
||||
if entity.unique_id not in known_entity_unique_ids
|
||||
)
|
||||
for event_key in (
|
||||
@@ -80,6 +112,7 @@ def _handle_paired_or_connected_appliance(
|
||||
entry.runtime_data.async_add_listener(
|
||||
partial(
|
||||
_create_option_entities,
|
||||
entity_registry,
|
||||
entry,
|
||||
appliance,
|
||||
known_entity_unique_ids,
|
||||
@@ -120,13 +153,14 @@ def _handle_depaired_appliance(
|
||||
|
||||
|
||||
def setup_home_connect_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
get_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData], list[HomeConnectEntity]
|
||||
],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
get_option_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData],
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData, er.EntityRegistry],
|
||||
list[HomeConnectOptionEntity],
|
||||
]
|
||||
| None = None,
|
||||
@@ -141,6 +175,7 @@ def setup_home_connect_entry(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
_handle_paired_or_connected_appliance,
|
||||
hass,
|
||||
entry,
|
||||
known_entity_unique_ids,
|
||||
get_entities_for_appliance,
|
||||
|
||||
@@ -96,6 +96,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect light."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -11,12 +11,13 @@ from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.const import PERCENTAGE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .common import setup_home_connect_entry, should_add_option_entity
|
||||
from .const import DOMAIN, UNIT_MAP
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
|
||||
@@ -136,12 +137,15 @@ def _get_entities_for_appliance(
|
||||
def _get_option_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> list[HomeConnectOptionEntity]:
|
||||
"""Get a list of currently available option entities."""
|
||||
return [
|
||||
HomeConnectOptionNumberEntity(entry.runtime_data, appliance, description)
|
||||
for description in NUMBER_OPTIONS
|
||||
if description.key in appliance.options
|
||||
if should_add_option_entity(
|
||||
description, appliance, entity_registry, Platform.NUMBER
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -152,6 +156,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect number."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -11,11 +11,13 @@ from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.program import Execution
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .common import setup_home_connect_entry, should_add_option_entity
|
||||
from .const import (
|
||||
AVAILABLE_MAPS_ENUM,
|
||||
BEAN_AMOUNT_OPTIONS,
|
||||
@@ -358,12 +360,13 @@ def _get_entities_for_appliance(
|
||||
def _get_option_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> list[HomeConnectOptionEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
HomeConnectSelectOptionEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in PROGRAM_SELECT_OPTION_ENTITY_DESCRIPTIONS
|
||||
if desc.key in appliance.options
|
||||
if should_add_option_entity(desc, appliance, entity_registry, Platform.SELECT)
|
||||
]
|
||||
|
||||
|
||||
@@ -374,6 +377,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect select entities."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -115,7 +115,6 @@ SENSORS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="hot_water_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
@@ -540,6 +539,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect sensor."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -7,12 +7,14 @@ from aiohomeconnect.model import OptionKey, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .common import setup_home_connect_entry, should_add_option_entity
|
||||
from .const import BSH_POWER_OFF, BSH_POWER_ON, BSH_POWER_STANDBY, DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
@@ -190,12 +192,15 @@ def _get_entities_for_appliance(
|
||||
def _get_option_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> list[HomeConnectOptionEntity]:
|
||||
"""Get a list of currently available option entities."""
|
||||
return [
|
||||
HomeConnectSwitchOptionEntity(entry.runtime_data, appliance, description)
|
||||
for description in SWITCH_OPTIONS
|
||||
if description.key in appliance.options
|
||||
if should_add_option_entity(
|
||||
description, appliance, entity_registry, Platform.SWITCH
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -206,6 +211,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Home Connect switch."""
|
||||
setup_home_connect_entry(
|
||||
hass,
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
"""The Husqvarna Automower integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from aioautomower.session import AutomowerSession
|
||||
from aiohttp import ClientResponseError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AutomowerConfigEntry, AutomowerDataUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
@@ -30,6 +34,12 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
implementation = (
|
||||
|
||||
@@ -142,3 +142,6 @@ ERROR_KEYS = [
|
||||
"wrong_pin_code",
|
||||
"zone_generator_problem",
|
||||
]
|
||||
|
||||
MOW = "mow"
|
||||
PARK = "park"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
"""Husqvarna Automower lawn mower entity."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aioautomower.model import MowerActivities, MowerStates, WorkArea
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lawn_mower import (
|
||||
LawnMowerActivity,
|
||||
@@ -14,16 +12,13 @@ from homeassistant.components.lawn_mower import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AutomowerConfigEntry
|
||||
from .const import DOMAIN, ERROR_STATES
|
||||
from .const import DOMAIN, ERROR_STATES, MOW, PARK
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import AutomowerBaseEntity, handle_sending_exception
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING)
|
||||
@@ -41,9 +36,6 @@ SUPPORT_STATE_SERVICES = (
|
||||
| LawnMowerEntityFeature.PAUSE
|
||||
| LawnMowerEntityFeature.START_MOWING
|
||||
)
|
||||
MOW = "mow"
|
||||
PARK = "park"
|
||||
OVERRIDE_MODES = [MOW, PARK]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -62,31 +54,6 @@ async def async_setup_entry(
|
||||
_async_add_new_devices(set(coordinator.data))
|
||||
|
||||
coordinator.new_devices_callbacks.append(_async_add_new_devices)
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
"override_schedule",
|
||||
{
|
||||
vol.Required("override_mode"): vol.In(OVERRIDE_MODES),
|
||||
vol.Required("duration"): vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta,
|
||||
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
|
||||
),
|
||||
},
|
||||
"async_override_schedule",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
"override_schedule_work_area",
|
||||
{
|
||||
vol.Required("work_area_id"): vol.Coerce(int),
|
||||
vol.Required("duration"): vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta,
|
||||
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
|
||||
),
|
||||
},
|
||||
"async_override_schedule_work_area",
|
||||
)
|
||||
|
||||
|
||||
class AutomowerLawnMowerEntity(AutomowerBaseEntity, LawnMowerEntity):
|
||||
|
||||
49
homeassistant/components/husqvarna_automower/services.py
Normal file
49
homeassistant/components/husqvarna_automower/services.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Husqvarna Automower services."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lawn_mower import DOMAIN as LAWN_MOWER_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN, MOW, PARK
|
||||
|
||||
OVERRIDE_MODES = [MOW, PARK]
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"override_schedule",
|
||||
entity_domain=LAWN_MOWER_DOMAIN,
|
||||
schema={
|
||||
vol.Required("override_mode"): vol.In(OVERRIDE_MODES),
|
||||
vol.Required("duration"): vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta,
|
||||
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
|
||||
),
|
||||
},
|
||||
func="async_override_schedule",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"override_schedule_work_area",
|
||||
entity_domain=LAWN_MOWER_DOMAIN,
|
||||
schema={
|
||||
vol.Required("work_area_id"): vol.Coerce(int),
|
||||
vol.Required("duration"): vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta,
|
||||
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
|
||||
),
|
||||
},
|
||||
func="async_override_schedule_work_area",
|
||||
)
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["incomfort-client==0.6.11"]
|
||||
"requirements": ["incomfort-client==0.6.12"]
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"boiler_int": "Boiler internal",
|
||||
"buffer": "Buffer",
|
||||
"central_heating": "Central heating",
|
||||
"central_heating_low": "Central heating low",
|
||||
"central_heating_rf": "Central heating rf",
|
||||
"cv_temperature_too_high_e1": "Temperature too high",
|
||||
"flame_detection_fault_e6": "Flame detection fault",
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
"name": "[%key:common::action::reload%]"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles the helper on/off.",
|
||||
"description": "Toggles an input boolean on/off.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off the helper.",
|
||||
"description": "Turns off an input boolean.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on the helper.",
|
||||
"description": "Turns on an input boolean.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"services": {
|
||||
"press": {
|
||||
"description": "Mimics the physical button press on the device.",
|
||||
"description": "Mimics a physical button press on a device.",
|
||||
"name": "Press"
|
||||
},
|
||||
"reload": {
|
||||
|
||||
@@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.script import CONF_MODE
|
||||
from homeassistant.const import CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
intent,
|
||||
@@ -18,6 +19,7 @@ from homeassistant.helpers import (
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.helpers.script import async_validate_actions_config
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -85,19 +87,29 @@ async def async_reload(hass: HomeAssistant, service_call: ServiceCall) -> None:
|
||||
|
||||
new_intents = new_config[DOMAIN]
|
||||
|
||||
async_load_intents(hass, new_intents)
|
||||
await async_load_intents(hass, new_intents)
|
||||
|
||||
|
||||
def async_load_intents(hass: HomeAssistant, intents: dict[str, ConfigType]) -> None:
|
||||
async def async_load_intents(
|
||||
hass: HomeAssistant, intents: dict[str, ConfigType]
|
||||
) -> None:
|
||||
"""Load YAML intents into the intent system."""
|
||||
hass.data[DOMAIN] = intents
|
||||
|
||||
for intent_type, conf in intents.items():
|
||||
if CONF_ACTION in conf:
|
||||
try:
|
||||
actions = await async_validate_actions_config(hass, conf[CONF_ACTION])
|
||||
except (vol.Invalid, HomeAssistantError) as exc:
|
||||
_LOGGER.error(
|
||||
"Failed to validate actions for intent %s: %s", intent_type, exc
|
||||
)
|
||||
continue # Skip this intent
|
||||
|
||||
script_mode: str = conf.get(CONF_MODE, script.DEFAULT_SCRIPT_MODE)
|
||||
conf[CONF_ACTION] = script.Script(
|
||||
hass,
|
||||
conf[CONF_ACTION],
|
||||
actions,
|
||||
f"Intent Script {intent_type}",
|
||||
DOMAIN,
|
||||
script_mode=script_mode,
|
||||
@@ -109,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the intent script component."""
|
||||
intents = config[DOMAIN]
|
||||
|
||||
async_load_intents(hass, intents)
|
||||
await async_load_intents(hass, intents)
|
||||
|
||||
async def _handle_reload(service_call: ServiceCall) -> None:
|
||||
return await async_reload(hass, service_call)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["iometer==0.3.0"],
|
||||
"requirements": ["iometer==0.4.0"],
|
||||
"zeroconf": ["_iometer._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==2.0.0"]
|
||||
"requirements": ["pyjvcprojector==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from propcache.api import cached_property
|
||||
from xknx.devices import Fan as XknxFan
|
||||
from xknx.telegram.address import parse_device_group_address
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@@ -37,6 +40,58 @@ from .storage.const import (
|
||||
)
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_yaml_uids(
|
||||
hass: HomeAssistant, platform_config: list[ConfigType]
|
||||
) -> None:
|
||||
"""Migrate entities unique_id for YAML switch-only fan entities."""
|
||||
# issue was introduced in 2026.1 - this migration in 2026.2
|
||||
ent_reg = er.async_get(hass)
|
||||
invalid_uid = str(None)
|
||||
if (
|
||||
none_entity_id := ent_reg.async_get_entity_id(Platform.FAN, DOMAIN, invalid_uid)
|
||||
) is None:
|
||||
return
|
||||
for config in platform_config:
|
||||
if not config.get(KNX_ADDRESS) and (
|
||||
new_uid_base := config.get(FanSchema.CONF_SWITCH_ADDRESS)
|
||||
):
|
||||
break
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"No YAML entry found to migrate fan entity '%s' unique_id from '%s'. Removing entry",
|
||||
none_entity_id,
|
||||
invalid_uid,
|
||||
)
|
||||
ent_reg.async_remove(none_entity_id)
|
||||
return
|
||||
new_uid = str(
|
||||
parse_device_group_address(
|
||||
new_uid_base[0], # list of group addresses - first item is sending address
|
||||
)
|
||||
)
|
||||
try:
|
||||
ent_reg.async_update_entity(none_entity_id, new_unique_id=str(new_uid))
|
||||
_LOGGER.info(
|
||||
"Migrating fan entity '%s' unique_id from '%s' to %s",
|
||||
none_entity_id,
|
||||
invalid_uid,
|
||||
new_uid,
|
||||
)
|
||||
except ValueError:
|
||||
# New unique_id already exists - remove invalid entry. User might have changed YAML
|
||||
_LOGGER.info(
|
||||
"Failed to migrate fan entity '%s' unique_id from '%s' to '%s'. "
|
||||
"Removing the invalid entry",
|
||||
none_entity_id,
|
||||
invalid_uid,
|
||||
new_uid,
|
||||
)
|
||||
ent_reg.async_remove(none_entity_id)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -57,6 +112,7 @@ async def async_setup_entry(
|
||||
|
||||
entities: list[_KnxFan] = []
|
||||
if yaml_platform_config := knx_module.config_yaml.get(Platform.FAN):
|
||||
async_migrate_yaml_uids(hass, yaml_platform_config)
|
||||
entities.extend(
|
||||
KnxYamlFan(knx_module, entity_config)
|
||||
for entity_config in yaml_platform_config
|
||||
@@ -177,7 +233,10 @@ class KnxYamlFan(_KnxFan, KnxYamlEntity):
|
||||
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
|
||||
self._attr_unique_id = str(self._device.speed.group_address)
|
||||
if self._device.speed.group_address:
|
||||
self._attr_unique_id = str(self._device.speed.group_address)
|
||||
else:
|
||||
self._attr_unique_id = str(self._device.switch.group_address)
|
||||
|
||||
|
||||
class KnxUiFan(_KnxFan, KnxUiEntity):
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,12 +12,12 @@ from pyliebherrhomeapi.exceptions import (
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> bool:
|
||||
@@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) ->
|
||||
try:
|
||||
devices = await client.get_devices()
|
||||
except LiebherrAuthenticationError as err:
|
||||
raise ConfigEntryError("Invalid API key") from err
|
||||
raise ConfigEntryAuthFailed("Invalid API key") from err
|
||||
except LiebherrConnectionError as err:
|
||||
raise ConfigEntryNotReady(f"Failed to connect to Liebherr API: {err}") from err
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -30,6 +31,25 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
class LiebherrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for liebherr."""
|
||||
|
||||
async def _validate_api_key(self, api_key: str) -> tuple[list, dict[str, str]]:
|
||||
"""Validate the API key and return devices and errors."""
|
||||
errors: dict[str, str] = {}
|
||||
devices: list = []
|
||||
client = LiebherrClient(
|
||||
api_key=api_key,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
devices = await client.get_devices()
|
||||
except LiebherrAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except LiebherrConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
return devices, errors
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -40,21 +60,8 @@ class LiebherrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
|
||||
|
||||
try:
|
||||
# Create a client and test the connection
|
||||
client = LiebherrClient(
|
||||
api_key=user_input[CONF_API_KEY],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
devices = await client.get_devices()
|
||||
except LiebherrAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except LiebherrConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
devices, errors = await self._validate_api_key(user_input[CONF_API_KEY])
|
||||
if not errors:
|
||||
if not devices:
|
||||
return self.async_abort(reason="no_devices")
|
||||
|
||||
@@ -66,3 +73,31 @@ class LiebherrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle re-authentication."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm re-authentication."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
api_key = user_input[CONF_API_KEY].strip()
|
||||
|
||||
_, errors = await self._validate_api_key(api_key)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates={CONF_API_KEY: api_key},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,11 @@ from pyliebherrhomeapi import (
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -64,7 +68,7 @@ class LiebherrCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
try:
|
||||
return await self.client.get_device_state(self.device_id)
|
||||
except LiebherrAuthenticationError as err:
|
||||
raise ConfigEntryError("API key is no longer valid") from err
|
||||
raise ConfigEntryAuthFailed("API key is no longer valid") from err
|
||||
except LiebherrTimeoutError as err:
|
||||
raise UpdateFailed(
|
||||
f"Timeout communicating with device {self.device_id}"
|
||||
|
||||
164
homeassistant/components/liebherr/number.py
Normal file
164
homeassistant/components/liebherr/number.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Number platform for Liebherr integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyliebherrhomeapi import (
|
||||
LiebherrConnectionError,
|
||||
LiebherrTimeoutError,
|
||||
TemperatureControl,
|
||||
TemperatureUnit,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
|
||||
from .entity import LiebherrZoneEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LiebherrNumberEntityDescription(NumberEntityDescription):
|
||||
"""Describes Liebherr number entity."""
|
||||
|
||||
value_fn: Callable[[TemperatureControl], float | None]
|
||||
min_fn: Callable[[TemperatureControl], float | None]
|
||||
max_fn: Callable[[TemperatureControl], float | None]
|
||||
unit_fn: Callable[[TemperatureControl], str]
|
||||
|
||||
|
||||
NUMBER_TYPES: tuple[LiebherrNumberEntityDescription, ...] = (
|
||||
LiebherrNumberEntityDescription(
|
||||
key="setpoint_temperature",
|
||||
translation_key="setpoint_temperature",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
native_step=1,
|
||||
value_fn=lambda control: control.target,
|
||||
min_fn=lambda control: control.min,
|
||||
max_fn=lambda control: control.max,
|
||||
unit_fn=lambda control: (
|
||||
UnitOfTemperature.FAHRENHEIT
|
||||
if control.unit == TemperatureUnit.FAHRENHEIT
|
||||
else UnitOfTemperature.CELSIUS
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LiebherrConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Liebherr number entities."""
|
||||
coordinators = entry.runtime_data
|
||||
async_add_entities(
|
||||
LiebherrNumber(
|
||||
coordinator=coordinator,
|
||||
zone_id=temp_control.zone_id,
|
||||
description=description,
|
||||
)
|
||||
for coordinator in coordinators.values()
|
||||
for temp_control in coordinator.data.get_temperature_controls().values()
|
||||
for description in NUMBER_TYPES
|
||||
)
|
||||
|
||||
|
||||
class LiebherrNumber(LiebherrZoneEntity, NumberEntity):
|
||||
"""Representation of a Liebherr number entity."""
|
||||
|
||||
entity_description: LiebherrNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LiebherrCoordinator,
|
||||
zone_id: int,
|
||||
description: LiebherrNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the number entity."""
|
||||
super().__init__(coordinator, zone_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.device_id}_{description.key}_{zone_id}"
|
||||
|
||||
# If device has only one zone, use translation key without zone suffix
|
||||
temp_controls = coordinator.data.get_temperature_controls()
|
||||
if len(temp_controls) > 1 and (zone_key := self._get_zone_translation_key()):
|
||||
self._attr_translation_key = f"{description.translation_key}_{zone_key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if (temp_control := self.temperature_control) is None:
|
||||
return None
|
||||
return self.entity_description.unit_fn(temp_control)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
# temperature_control is guaranteed to exist when entity is available
|
||||
return self.entity_description.value_fn(
|
||||
self.temperature_control # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_min_value(self) -> float:
|
||||
"""Return the minimum value."""
|
||||
if (temp_control := self.temperature_control) is None:
|
||||
return DEFAULT_MIN_VALUE
|
||||
if (min_val := self.entity_description.min_fn(temp_control)) is None:
|
||||
return DEFAULT_MIN_VALUE
|
||||
return min_val
|
||||
|
||||
@property
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
if (temp_control := self.temperature_control) is None:
|
||||
return DEFAULT_MAX_VALUE
|
||||
if (max_val := self.entity_description.max_fn(temp_control)) is None:
|
||||
return DEFAULT_MAX_VALUE
|
||||
return max_val
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.temperature_control is not None
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
# temperature_control is guaranteed to exist when entity is available
|
||||
temp_control = self.temperature_control
|
||||
|
||||
unit = (
|
||||
TemperatureUnit.FAHRENHEIT
|
||||
if temp_control.unit == TemperatureUnit.FAHRENHEIT # type: ignore[union-attr]
|
||||
else TemperatureUnit.CELSIUS
|
||||
)
|
||||
|
||||
try:
|
||||
await self.coordinator.client.set_temperature(
|
||||
device_id=self.coordinator.device_id,
|
||||
zone_id=self._zone_id,
|
||||
target=int(value),
|
||||
unit=unit,
|
||||
)
|
||||
except (LiebherrConnectionError, LiebherrTimeoutError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_temperature_failed",
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -36,7 +36,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
@@ -60,7 +60,9 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: The only configuration option is the API key, which is handled by the reauthentication flow.
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repair issues to implement at this time.
|
||||
|
||||
@@ -55,23 +55,16 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Liebherr sensor entities."""
|
||||
coordinators = entry.runtime_data
|
||||
entities: list[LiebherrSensor] = []
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
# Get all temperature controls for this device
|
||||
temp_controls = coordinator.data.get_temperature_controls()
|
||||
|
||||
for temp_control in temp_controls.values():
|
||||
entities.extend(
|
||||
LiebherrSensor(
|
||||
coordinator=coordinator,
|
||||
zone_id=temp_control.zone_id,
|
||||
description=description,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
LiebherrSensor(
|
||||
coordinator=coordinator,
|
||||
zone_id=temp_control.zone_id,
|
||||
description=description,
|
||||
)
|
||||
for coordinator in coordinators.values()
|
||||
for temp_control in coordinator.data.get_temperature_controls().values()
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
class LiebherrSensor(LiebherrZoneEntity, SensorEntity):
|
||||
@@ -108,9 +101,9 @@ class LiebherrSensor(LiebherrZoneEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the current value."""
|
||||
if (temp_control := self.temperature_control) is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(temp_control)
|
||||
# temperature_control is guaranteed to exist when entity is available
|
||||
assert self.temperature_control is not None
|
||||
return self.entity_description.value_fn(self.temperature_control)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_devices": "No devices found for this API key"
|
||||
"no_devices": "No devices found for this API key",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -11,6 +12,15 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::liebherr::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "Your API key is no longer valid. Please enter a new API key to continue."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
@@ -23,6 +33,20 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"setpoint_temperature": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"setpoint_temperature_bottom_zone": {
|
||||
"name": "Bottom zone setpoint"
|
||||
},
|
||||
"setpoint_temperature_middle_zone": {
|
||||
"name": "Middle zone setpoint"
|
||||
},
|
||||
"setpoint_temperature_top_zone": {
|
||||
"name": "Top zone setpoint"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"bottom_zone": {
|
||||
"name": "Bottom zone"
|
||||
@@ -34,5 +58,10 @@
|
||||
"name": "Top zone"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"set_temperature_failed": {
|
||||
"message": "Failed to set temperature"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,9 +336,9 @@
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "[%key:common::selector::trigger_behavior::options::any%]",
|
||||
"first": "[%key:common::selector::trigger_behavior::options::first%]",
|
||||
"last": "[%key:common::selector::trigger_behavior::options::last%]"
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
|
||||
@@ -6,11 +6,15 @@ import itertools
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LitterRobotConfigEntry, LitterRobotDataUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
@@ -23,6 +27,12 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LitterRobotConfigEntry) -> bool:
|
||||
"""Set up Litter-Robot from a config entry."""
|
||||
coordinator = LitterRobotDataUpdateCoordinator(hass, entry)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user