forked from home-assistant/core
Compare commits
50 Commits
gj-2025061
...
aioesphome
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4efd31b7d | ||
|
|
db3090078b | ||
|
|
66e2fd997b | ||
|
|
a102eaf0cd | ||
|
|
f3533dff44 | ||
|
|
c453eed32d | ||
|
|
79a9f34150 | ||
|
|
7442f7af28 | ||
|
|
2e5de732a7 | ||
|
|
9bcd74c449 | ||
|
|
ace18e540b | ||
|
|
65f897793d | ||
|
|
435c08685d | ||
|
|
95f292c43d | ||
|
|
9346c584c3 | ||
|
|
6738085391 | ||
|
|
d9e5bad55e | ||
|
|
f7429f3431 | ||
|
|
46aea5d9dc | ||
|
|
33bde48c9c | ||
|
|
1b73acc025 | ||
|
|
e28965770e | ||
|
|
f9d4bde0f6 | ||
|
|
a493bdc208 | ||
|
|
9ae9ad1e43 | ||
|
|
1b60ea8951 | ||
|
|
313eaff14e | ||
|
|
7dfd68f8c0 | ||
|
|
544fd2a4a6 | ||
|
|
cd51070219 | ||
|
|
3c91c78383 | ||
|
|
96e0d1f5c6 | ||
|
|
2859e7de9b | ||
|
|
88683a318d | ||
|
|
84e9422254 | ||
|
|
fde36d5034 | ||
|
|
8c1e43c07c | ||
|
|
05343392a7 | ||
|
|
32314dbb13 | ||
|
|
8f661fc5cf | ||
|
|
e315cb9859 | ||
|
|
d0e77eb1e2 | ||
|
|
e23cac8bef | ||
|
|
973700542b | ||
|
|
2e21493c19 | ||
|
|
73bed96a0f | ||
|
|
0a5d13f104 | ||
|
|
d16ec81727 | ||
|
|
11564e3df5 | ||
|
|
341d9f15f0 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -57,6 +57,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/aemet/ @Noltari
|
||||
/homeassistant/components/agent_dvr/ @ispysoftware
|
||||
/tests/components/agent_dvr/ @ispysoftware
|
||||
/homeassistant/components/ai_task/ @home-assistant/core
|
||||
/tests/components/ai_task/ @home-assistant/core
|
||||
/homeassistant/components/air_quality/ @home-assistant/core
|
||||
/tests/components/air_quality/ @home-assistant/core
|
||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||
|
||||
125
homeassistant/components/ai_task/__init__.py
Normal file
125
homeassistant/components/ai_task/__init__.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Integration to offer AI tasks to Home Assistant."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import (
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, storage
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature
|
||||
from .entity import AITaskEntity
|
||||
from .http import async_setup as async_setup_conversation_http
|
||||
from .task import GenTextTask, GenTextTaskResult, async_generate_text
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"AITaskEntity",
|
||||
"AITaskEntityFeature",
|
||||
"GenTextTask",
|
||||
"GenTextTaskResult",
|
||||
"async_generate_text",
|
||||
"async_setup",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Register the process service."""
|
||||
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
||||
hass.data[DATA_COMPONENT] = entity_component
|
||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||
await hass.data[DATA_PREFERENCES].async_load()
|
||||
async_setup_conversation_http(hass)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"generate_text",
|
||||
async_service_generate_text,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required("task_name"): cv.string,
|
||||
vol.Optional("entity_id"): cv.entity_id,
|
||||
vol.Required("instructions"): cv.string,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
job_type=HassJobType.Coroutinefunction,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||
|
||||
|
||||
async def async_service_generate_text(call: ServiceCall) -> ServiceResponse:
|
||||
"""Run the run task service."""
|
||||
result = await async_generate_text(hass=call.hass, **call.data)
|
||||
return result.as_dict() # type: ignore[return-value]
|
||||
|
||||
|
||||
class AITaskPreferences:
|
||||
"""AI Task preferences."""
|
||||
|
||||
KEYS = ("gen_text_entity_id",)
|
||||
|
||||
gen_text_entity_id: str | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the preferences."""
|
||||
self._store: storage.Store[dict[str, str | None]] = storage.Store(
|
||||
hass, 1, DOMAIN
|
||||
)
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the data from the store."""
|
||||
data = await self._store.async_load()
|
||||
if data is None:
|
||||
return
|
||||
for key in self.KEYS:
|
||||
setattr(self, key, data[key])
|
||||
|
||||
@callback
|
||||
def async_set_preferences(
|
||||
self,
|
||||
*,
|
||||
gen_text_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Set the preferences."""
|
||||
changed = False
|
||||
for key, value in (("gen_text_entity_id", gen_text_entity_id),):
|
||||
if value is not UNDEFINED:
|
||||
if getattr(self, key) != value:
|
||||
setattr(self, key, value)
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
return
|
||||
|
||||
self._store.async_delay_save(self.as_dict, 10)
|
||||
|
||||
@callback
|
||||
def as_dict(self) -> dict[str, str | None]:
|
||||
"""Get the current preferences."""
|
||||
return {key: getattr(self, key) for key in self.KEYS}
|
||||
29
homeassistant/components/ai_task/const.py
Normal file
29
homeassistant/components/ai_task/const.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Constants for the AI Task integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntFlag
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from . import AITaskPreferences
|
||||
from .entity import AITaskEntity
|
||||
|
||||
DOMAIN = "ai_task"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||
|
||||
DEFAULT_SYSTEM_PROMPT = (
|
||||
"You are a Home Assistant expert and help users with their tasks."
|
||||
)
|
||||
|
||||
|
||||
class AITaskEntityFeature(IntFlag):
|
||||
"""Supported features of the AI task entity."""
|
||||
|
||||
GENERATE_TEXT = 1
|
||||
"""Generate text based on instructions."""
|
||||
103
homeassistant/components/ai_task/entity.py
Normal file
103
homeassistant/components/ai_task/entity.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Entity for the AI Task integration."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import contextlib
|
||||
from typing import final
|
||||
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components.conversation import (
|
||||
ChatLog,
|
||||
UserContent,
|
||||
async_get_chat_log,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
||||
from .task import GenTextTask, GenTextTaskResult
|
||||
|
||||
|
||||
class AITaskEntity(RestoreEntity):
|
||||
"""Entity that supports conversations."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = AITaskEntityFeature(0)
|
||||
__last_activity: str | None = None
|
||||
|
||||
@property
|
||||
@final
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the entity."""
|
||||
if self.__last_activity is None:
|
||||
return None
|
||||
return self.__last_activity
|
||||
|
||||
@cached_property
|
||||
def supported_features(self) -> AITaskEntityFeature:
|
||||
"""Flag supported features."""
|
||||
return self._attr_supported_features
|
||||
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the entity is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if (
|
||||
state is not None
|
||||
and state.state is not None
|
||||
and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
):
|
||||
self.__last_activity = state.state
|
||||
|
||||
@final
|
||||
@contextlib.asynccontextmanager
|
||||
async def _async_get_ai_task_chat_log(
|
||||
self,
|
||||
task: GenTextTask,
|
||||
) -> AsyncGenerator[ChatLog]:
|
||||
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||
with (
|
||||
async_get_chat_session(self.hass) as session,
|
||||
async_get_chat_log(
|
||||
self.hass,
|
||||
session,
|
||||
None,
|
||||
) as chat_log,
|
||||
):
|
||||
await chat_log.async_provide_llm_data(
|
||||
llm.LLMContext(
|
||||
platform=self.platform.domain,
|
||||
context=None,
|
||||
language=None,
|
||||
assistant=DOMAIN,
|
||||
device_id=None,
|
||||
),
|
||||
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
||||
)
|
||||
|
||||
chat_log.async_add_user_content(UserContent(task.instructions))
|
||||
|
||||
yield chat_log
|
||||
|
||||
@final
|
||||
async def internal_async_generate_text(
|
||||
self,
|
||||
task: GenTextTask,
|
||||
) -> GenTextTaskResult:
|
||||
"""Run a gen text task."""
|
||||
self.__last_activity = dt_util.utcnow().isoformat()
|
||||
self.async_write_ha_state()
|
||||
async with self._async_get_ai_task_chat_log(task) as chat_log:
|
||||
return await self._async_generate_text(task, chat_log)
|
||||
|
||||
async def _async_generate_text(
|
||||
self,
|
||||
task: GenTextTask,
|
||||
chat_log: ChatLog,
|
||||
) -> GenTextTaskResult:
|
||||
"""Handle a gen text task."""
|
||||
raise NotImplementedError
|
||||
54
homeassistant/components/ai_task/http.py
Normal file
54
homeassistant/components/ai_task/http.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""HTTP endpoint for AI Task integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import DATA_PREFERENCES
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the HTTP API for the conversation integration."""
|
||||
websocket_api.async_register_command(hass, websocket_get_preferences)
|
||||
websocket_api.async_register_command(hass, websocket_set_preferences)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "ai_task/preferences/get",
|
||||
}
|
||||
)
|
||||
@callback
|
||||
def websocket_get_preferences(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get AI task preferences."""
|
||||
preferences = hass.data[DATA_PREFERENCES]
|
||||
connection.send_result(msg["id"], preferences.as_dict())
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "ai_task/preferences/set",
|
||||
vol.Optional("gen_text_entity_id"): vol.Any(str, None),
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@callback
|
||||
def websocket_set_preferences(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Set AI task preferences."""
|
||||
preferences = hass.data[DATA_PREFERENCES]
|
||||
msg.pop("type")
|
||||
msg_id = msg.pop("id")
|
||||
preferences.async_set_preferences(**msg)
|
||||
connection.send_result(msg_id, preferences.as_dict())
|
||||
7
homeassistant/components/ai_task/icons.json
Normal file
7
homeassistant/components/ai_task/icons.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"generate_text": {
|
||||
"service": "mdi:file-star-four-points-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
homeassistant/components/ai_task/manifest.json
Normal file
9
homeassistant/components/ai_task/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"domain": "ai_task",
|
||||
"name": "AI Task",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["conversation"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
19
homeassistant/components/ai_task/services.yaml
Normal file
19
homeassistant/components/ai_task/services.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
generate_text:
|
||||
fields:
|
||||
task_name:
|
||||
example: "home summary"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
instructions:
|
||||
example: "Generate a funny notification that garage door was left open"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
entity_id:
|
||||
required: false
|
||||
selector:
|
||||
entity:
|
||||
domain: ai_task
|
||||
supported_features:
|
||||
- ai_task.AITaskEntityFeature.GENERATE_TEXT
|
||||
22
homeassistant/components/ai_task/strings.json
Normal file
22
homeassistant/components/ai_task/strings.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"services": {
|
||||
"generate_text": {
|
||||
"name": "Generate text",
|
||||
"description": "Use AI to run a task that generates text.",
|
||||
"fields": {
|
||||
"task_name": {
|
||||
"name": "Task Name",
|
||||
"description": "Name of the task."
|
||||
},
|
||||
"instructions": {
|
||||
"name": "Instructions",
|
||||
"description": "Instructions on what needs to be done."
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
homeassistant/components/ai_task/task.py
Normal file
71
homeassistant/components/ai_task/task.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""AI tasks to be handled by agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||
|
||||
|
||||
async def async_generate_text(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
task_name: str,
|
||||
entity_id: str | None = None,
|
||||
instructions: str,
|
||||
) -> GenTextTaskResult:
|
||||
"""Run a task in the AI Task integration."""
|
||||
if entity_id is None:
|
||||
entity_id = hass.data[DATA_PREFERENCES].gen_text_entity_id
|
||||
|
||||
if entity_id is None:
|
||||
raise ValueError("No entity_id provided and no preferred entity set")
|
||||
|
||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||
if entity is None:
|
||||
raise ValueError(f"AI Task entity {entity_id} not found")
|
||||
|
||||
if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features:
|
||||
raise ValueError(f"AI Task entity {entity_id} does not support generating text")
|
||||
|
||||
return await entity.internal_async_generate_text(
|
||||
GenTextTask(
|
||||
name=task_name,
|
||||
instructions=instructions,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenTextTask:
|
||||
"""Gen text task to be processed."""
|
||||
|
||||
name: str
|
||||
"""Name of the task."""
|
||||
|
||||
instructions: str
|
||||
"""Instructions on what needs to be done."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return task as a string."""
|
||||
return f"<GenTextTask {self.name}: {id(self)}>"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenTextTaskResult:
|
||||
"""Result of gen text task."""
|
||||
|
||||
conversation_id: str
|
||||
"""Unique identifier for the conversation."""
|
||||
|
||||
text: str
|
||||
"""Generated text."""
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
"""Return result as a dict."""
|
||||
return {
|
||||
"conversation_id": self.conversation_id,
|
||||
"text": self.text,
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.12"]
|
||||
"requirements": ["aioamazondevices==3.1.14"]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from hassil.util import (
|
||||
PUNCTUATION_END,
|
||||
PUNCTUATION_END_WORD,
|
||||
PUNCTUATION_START,
|
||||
PUNCTUATION_START_WORD,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -23,6 +33,7 @@ from .const import (
|
||||
)
|
||||
from .entity import (
|
||||
AssistSatelliteAnnouncement,
|
||||
AssistSatelliteAnswer,
|
||||
AssistSatelliteConfiguration,
|
||||
AssistSatelliteEntity,
|
||||
AssistSatelliteEntityDescription,
|
||||
@@ -34,6 +45,7 @@ from .websocket_api import async_register_websocket_api
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"AssistSatelliteAnnouncement",
|
||||
"AssistSatelliteAnswer",
|
||||
"AssistSatelliteConfiguration",
|
||||
"AssistSatelliteEntity",
|
||||
"AssistSatelliteEntityDescription",
|
||||
@@ -86,6 +98,62 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"async_internal_start_conversation",
|
||||
[AssistSatelliteEntityFeature.START_CONVERSATION],
|
||||
)
|
||||
|
||||
async def handle_ask_question(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Handle a Show View service call."""
|
||||
satellite_entity_id: str = call.data[ATTR_ENTITY_ID]
|
||||
satellite_entity: AssistSatelliteEntity | None = component.get_entity(
|
||||
satellite_entity_id
|
||||
)
|
||||
if satellite_entity is None:
|
||||
raise HomeAssistantError(
|
||||
f"Invalid Assist satellite entity id: {satellite_entity_id}"
|
||||
)
|
||||
|
||||
ask_question_args = {
|
||||
"question": call.data.get("question"),
|
||||
"question_media_id": call.data.get("question_media_id"),
|
||||
"preannounce": call.data.get("preannounce", False),
|
||||
"answers": call.data.get("answers"),
|
||||
}
|
||||
|
||||
if preannounce_media_id := call.data.get("preannounce_media_id"):
|
||||
ask_question_args["preannounce_media_id"] = preannounce_media_id
|
||||
|
||||
answer = await satellite_entity.async_internal_ask_question(**ask_question_args)
|
||||
|
||||
if answer is None:
|
||||
raise HomeAssistantError("No answer from satellite")
|
||||
|
||||
return asdict(answer)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service="ask_question",
|
||||
service_func=handle_ask_question,
|
||||
schema=vol.All(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||
vol.Optional("question"): str,
|
||||
vol.Optional("question_media_id"): str,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("answers"): [
|
||||
{
|
||||
vol.Required("id"): str,
|
||||
vol.Required("sentences"): vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string],
|
||||
has_one_non_empty_item,
|
||||
has_no_punctuation,
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
cv.has_at_least_one_key("question", "question_media_id"),
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.data[CONNECTION_TEST_DATA] = {}
|
||||
async_register_websocket_api(hass)
|
||||
hass.http.register_view(ConnectionTestView())
|
||||
@@ -110,3 +178,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||
|
||||
|
||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
"""Validate result does not contain punctuation."""
|
||||
for sentence in value:
|
||||
if (
|
||||
PUNCTUATION_START.search(sentence)
|
||||
or PUNCTUATION_END.search(sentence)
|
||||
or PUNCTUATION_START_WORD.search(sentence)
|
||||
or PUNCTUATION_END_WORD.search(sentence)
|
||||
):
|
||||
raise vol.Invalid("sentence should not contain punctuation")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||
"""Validate result has at least one item."""
|
||||
if len(value) < 1:
|
||||
raise vol.Invalid("at least one sentence is required")
|
||||
|
||||
for sentence in value:
|
||||
if not sentence:
|
||||
raise vol.Invalid("sentences cannot be empty")
|
||||
|
||||
return value
|
||||
|
||||
@@ -4,12 +4,16 @@ from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Literal, final
|
||||
|
||||
from hassil import Intents, recognize
|
||||
from hassil.expression import Expression, ListReference, Sequence
|
||||
from hassil.intents import WildcardSlotList
|
||||
|
||||
from homeassistant.components import conversation, media_source, stt, tts
|
||||
from homeassistant.components.assist_pipeline import (
|
||||
OPTION_PREFERRED,
|
||||
@@ -105,6 +109,20 @@ class AssistSatelliteAnnouncement:
|
||||
"""Media ID to be played before announcement."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssistSatelliteAnswer:
|
||||
"""Answer to a question."""
|
||||
|
||||
id: str | None
|
||||
"""Matched answer id or None if no answer was matched."""
|
||||
|
||||
sentence: str
|
||||
"""Raw sentence text from user response."""
|
||||
|
||||
slots: dict[str, Any] = field(default_factory=dict)
|
||||
"""Matched slots from answer."""
|
||||
|
||||
|
||||
class AssistSatelliteEntity(entity.Entity):
|
||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||
|
||||
@@ -122,6 +140,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
|
||||
_attr_tts_options: dict[str, Any] | None = None
|
||||
_pipeline_task: asyncio.Task | None = None
|
||||
_ask_question_future: asyncio.Future[str | None] | None = None
|
||||
|
||||
__assist_satellite_state = AssistSatelliteState.IDLE
|
||||
|
||||
@@ -309,6 +328,112 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
"""Start a conversation from the satellite."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_internal_ask_question(
|
||||
self,
|
||||
question: str | None = None,
|
||||
question_media_id: str | None = None,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
answers: list[dict[str, Any]] | None = None,
|
||||
) -> AssistSatelliteAnswer | None:
|
||||
"""Ask a question and get a user's response from the satellite.
|
||||
|
||||
If question_media_id is not provided, question is synthesized to audio
|
||||
with the selected pipeline.
|
||||
|
||||
If question_media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce is True, a sound is played before the start message or media.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
|
||||
Calls async_start_conversation.
|
||||
"""
|
||||
await self._cancel_running_pipeline()
|
||||
|
||||
if question is None:
|
||||
question = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
question,
|
||||
question_media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
raise SatelliteBusyError
|
||||
|
||||
self._is_announcing = True
|
||||
self._set_state(AssistSatelliteState.RESPONDING)
|
||||
self._ask_question_future = asyncio.Future()
|
||||
|
||||
try:
|
||||
# Wait for announcement to finish
|
||||
await self.async_start_conversation(announcement)
|
||||
|
||||
# Wait for response text
|
||||
response_text = await self._ask_question_future
|
||||
if response_text is None:
|
||||
raise HomeAssistantError("No answer from question")
|
||||
|
||||
if not answers:
|
||||
return AssistSatelliteAnswer(id=None, sentence=response_text)
|
||||
|
||||
return self._question_response_to_answer(response_text, answers)
|
||||
finally:
|
||||
self._is_announcing = False
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
self._ask_question_future = None
|
||||
|
||||
def _question_response_to_answer(
|
||||
self, response_text: str, answers: list[dict[str, Any]]
|
||||
) -> AssistSatelliteAnswer:
|
||||
"""Match text to a pre-defined set of answers."""
|
||||
|
||||
# Build intents and match
|
||||
intents = Intents.from_dict(
|
||||
{
|
||||
"language": self.hass.config.language,
|
||||
"intents": {
|
||||
"QuestionIntent": {
|
||||
"data": [
|
||||
{
|
||||
"sentences": answer["sentences"],
|
||||
"metadata": {"answer_id": answer["id"]},
|
||||
}
|
||||
for answer in answers
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Assume slot list references are wildcards
|
||||
wildcard_names: set[str] = set()
|
||||
for intent in intents.intents.values():
|
||||
for intent_data in intent.data:
|
||||
for sentence in intent_data.sentences:
|
||||
_collect_list_references(sentence, wildcard_names)
|
||||
|
||||
for wildcard_name in wildcard_names:
|
||||
intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name)
|
||||
|
||||
# Match response text
|
||||
result = recognize(response_text, intents)
|
||||
if result is None:
|
||||
# No match
|
||||
return AssistSatelliteAnswer(id=None, sentence=response_text)
|
||||
|
||||
assert result.intent_metadata
|
||||
return AssistSatelliteAnswer(
|
||||
id=result.intent_metadata["answer_id"],
|
||||
sentence=response_text,
|
||||
slots={
|
||||
entity_name: entity.value
|
||||
for entity_name, entity in result.entities.items()
|
||||
},
|
||||
)
|
||||
|
||||
async def async_accept_pipeline_from_satellite(
|
||||
self,
|
||||
audio_stream: AsyncIterable[bytes],
|
||||
@@ -351,6 +476,11 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END))
|
||||
return
|
||||
|
||||
if (self._ask_question_future is not None) and (
|
||||
start_stage == PipelineStage.STT
|
||||
):
|
||||
end_stage = PipelineStage.STT
|
||||
|
||||
device_id = self.registry_entry.device_id if self.registry_entry else None
|
||||
|
||||
# Refresh context if necessary
|
||||
@@ -433,6 +563,16 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
elif event.type is PipelineEventType.STT_START:
|
||||
self._set_state(AssistSatelliteState.LISTENING)
|
||||
elif event.type is PipelineEventType.STT_END:
|
||||
# Intercepting text for ask question
|
||||
if (
|
||||
(self._ask_question_future is not None)
|
||||
and (not self._ask_question_future.done())
|
||||
and event.data
|
||||
):
|
||||
self._ask_question_future.set_result(
|
||||
event.data.get("stt_output", {}).get("text")
|
||||
)
|
||||
elif event.type is PipelineEventType.INTENT_START:
|
||||
self._set_state(AssistSatelliteState.PROCESSING)
|
||||
elif event.type is PipelineEventType.TTS_START:
|
||||
@@ -443,6 +583,12 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
if not self._run_has_tts:
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
|
||||
if (self._ask_question_future is not None) and (
|
||||
not self._ask_question_future.done()
|
||||
):
|
||||
# No text for ask question
|
||||
self._ask_question_future.set_result(None)
|
||||
|
||||
self.on_pipeline_event(event)
|
||||
|
||||
@callback
|
||||
@@ -577,3 +723,15 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
media_id_source=media_id_source,
|
||||
preannounce_media_id=preannounce_media_id,
|
||||
)
|
||||
|
||||
|
||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||
"""Collect list reference names recursively."""
|
||||
if isinstance(expression, Sequence):
|
||||
seq: Sequence = expression
|
||||
for item in seq.items:
|
||||
_collect_list_references(item, list_names)
|
||||
elif isinstance(expression, ListReference):
|
||||
# {list}
|
||||
list_ref: ListReference = expression
|
||||
list_names.add(list_ref.slot_name)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
},
|
||||
"start_conversation": {
|
||||
"service": "mdi:forum"
|
||||
},
|
||||
"ask_question": {
|
||||
"service": "mdi:microphone-question"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"dependencies": ["assist_pipeline", "http", "stt", "tts"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal"
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -54,3 +54,35 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
ask_question:
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
supported_features:
|
||||
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
|
||||
question:
|
||||
required: false
|
||||
example: "What kind of music would you like to play?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
question_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
answers:
|
||||
required: false
|
||||
selector:
|
||||
object:
|
||||
|
||||
@@ -59,6 +59,36 @@
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ask_question": {
|
||||
"name": "Ask question",
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Assist satellite entity to ask the question on."
|
||||
},
|
||||
"question": {
|
||||
"name": "Question",
|
||||
"description": "The question to ask."
|
||||
},
|
||||
"question_media_id": {
|
||||
"name": "Question media ID",
|
||||
"description": "The media ID of the question to use instead of text-to-speech."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the start message or media."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
},
|
||||
"answers": {
|
||||
"name": "Answers",
|
||||
"description": "Possible answers to the question."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,10 @@ async def _async_get_stream_image(
|
||||
height: int | None = None,
|
||||
wait_for_next_keyframe: bool = False,
|
||||
) -> bytes | None:
|
||||
if (provider := camera._webrtc_provider) and ( # noqa: SLF001
|
||||
image := await provider.async_get_image(camera, width=width, height=height)
|
||||
) is not None:
|
||||
return image
|
||||
if not camera.stream and CameraEntityFeature.STREAM in camera.supported_features:
|
||||
camera.stream = await camera.async_create_stream()
|
||||
if camera.stream:
|
||||
@@ -494,19 +498,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag supported features."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> CameraEntityFeature:
|
||||
"""Return the supported features as CameraEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = CameraEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
@cached_property
|
||||
def is_recording(self) -> bool:
|
||||
"""Return true if the device is recording."""
|
||||
@@ -700,9 +691,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
self.__supports_stream = (
|
||||
self.supported_features_compat & CameraEntityFeature.STREAM
|
||||
)
|
||||
self.__supports_stream = self.supported_features & CameraEntityFeature.STREAM
|
||||
await self.async_refresh_providers(write_state=False)
|
||||
|
||||
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
|
||||
@@ -731,7 +720,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]]
|
||||
) -> _T | None:
|
||||
"""Get first provider that supports this camera."""
|
||||
if CameraEntityFeature.STREAM not in self.supported_features_compat:
|
||||
if CameraEntityFeature.STREAM not in self.supported_features:
|
||||
return None
|
||||
|
||||
return await fn(self.hass, self)
|
||||
@@ -781,7 +770,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def camera_capabilities(self) -> CameraCapabilities:
|
||||
"""Return the camera capabilities."""
|
||||
frontend_stream_types = set()
|
||||
if CameraEntityFeature.STREAM in self.supported_features_compat:
|
||||
if CameraEntityFeature.STREAM in self.supported_features:
|
||||
if self._supports_native_async_webrtc:
|
||||
# The camera has a native WebRTC implementation
|
||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||
@@ -801,8 +790,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""
|
||||
super().async_write_ha_state()
|
||||
if self.__supports_stream != (
|
||||
supports_stream := self.supported_features_compat
|
||||
& CameraEntityFeature.STREAM
|
||||
supports_stream := self.supported_features & CameraEntityFeature.STREAM
|
||||
):
|
||||
self.__supports_stream = supports_stream
|
||||
self._invalidate_camera_capabilities_cache()
|
||||
|
||||
@@ -156,6 +156,15 @@ class CameraWebRTCProvider(ABC):
|
||||
"""Close the session."""
|
||||
return ## This is an optional method so we need a default here.
|
||||
|
||||
async def async_get_image(
|
||||
self,
|
||||
camera: Camera,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> bytes | None:
|
||||
"""Get an image from the camera."""
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_webrtc_provider(
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.102.0"],
|
||||
"requirements": ["hass-nabucasa==0.103.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import GATEWAY_SERIAL_PATTERN, PLATFORMS
|
||||
from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
|
||||
|
||||
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
|
||||
|
||||
@@ -32,10 +32,16 @@ async def async_setup_entry(
|
||||
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
|
||||
|
||||
if not credentials_valid:
|
||||
raise ConfigEntryAuthFailed
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
)
|
||||
|
||||
if await hass.async_add_executor_job(mydevolo.maintenance):
|
||||
raise ConfigEntryNotReady
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="maintenance",
|
||||
)
|
||||
|
||||
gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids)
|
||||
|
||||
@@ -69,7 +75,11 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
except GatewayOfflineError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_failed",
|
||||
translation_placeholders={"gateway_id": gateway_id},
|
||||
) from err
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -45,5 +45,16 @@
|
||||
"name": "Brightness"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"connection_failed": {
|
||||
"message": "Failed to connect to devolo Home Control central unit {gateway_id}."
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Authentication failed. Please re-authenticaticate with your mydevolo account."
|
||||
},
|
||||
"maintenance": {
|
||||
"message": "devolo Home Control is currently in maintenance mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent
|
||||
from deebot_client.events.base import Event
|
||||
from deebot_client.events.water_info import MopAttachedEvent
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -16,15 +16,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||
from .util import get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsBinarySensorEntityDescription(
|
||||
class EcovacsBinarySensorEntityDescription[EventT: Event](
|
||||
BinarySensorEntityDescription,
|
||||
EcovacsCapabilityEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Class describing Deebot binary sensor entity."""
|
||||
|
||||
@@ -55,7 +54,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class EcovacsBinarySensor(
|
||||
class EcovacsBinarySensor[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilityEvent[EventT]],
|
||||
BinarySensorEntity,
|
||||
):
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any
|
||||
|
||||
from deebot_client.capabilities import Capabilities
|
||||
from deebot_client.device import Device
|
||||
@@ -18,11 +18,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CapabilityEntity = TypeVar("CapabilityEntity")
|
||||
EventT = TypeVar("EventT", bound=Event)
|
||||
|
||||
|
||||
class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
class EcovacsEntity[CapabilityEntityT](Entity):
|
||||
"""Ecovacs entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
@@ -32,7 +29,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilityEntity,
|
||||
capability: CapabilityEntityT,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
@@ -80,7 +77,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
|
||||
self._subscribe(AvailabilityEvent, on_available)
|
||||
|
||||
def _subscribe(
|
||||
def _subscribe[EventT: Event](
|
||||
self,
|
||||
event_type: type[EventT],
|
||||
callback: Callable[[EventT], Coroutine[Any, Any, None]],
|
||||
@@ -98,13 +95,13 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
self._device.events.request_refresh(event_type)
|
||||
|
||||
|
||||
class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
|
||||
class EcovacsDescriptionEntity[CapabilityEntityT](EcovacsEntity[CapabilityEntityT]):
|
||||
"""Ecovacs entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilityEntity,
|
||||
capability: CapabilityEntityT,
|
||||
entity_description: EntityDescription,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
@@ -114,13 +111,12 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsCapabilityEntityDescription(
|
||||
class EcovacsCapabilityEntityDescription[CapabilityEntityT](
|
||||
EntityDescription,
|
||||
Generic[CapabilityEntity],
|
||||
):
|
||||
"""Ecovacs entity description."""
|
||||
|
||||
capability_fn: Callable[[Capabilities], CapabilityEntity | None]
|
||||
capability_fn: Callable[[Capabilities], CapabilityEntityT | None]
|
||||
|
||||
|
||||
class EcovacsLegacyEntity(Entity):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilitySet
|
||||
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
|
||||
from deebot_client.events.base import Event
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
@@ -23,16 +23,14 @@ from .entity import (
|
||||
EcovacsCapabilityEntityDescription,
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsNumberEntityDescription(
|
||||
class EcovacsNumberEntityDescription[EventT: Event](
|
||||
NumberEntityDescription,
|
||||
EcovacsCapabilityEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Ecovacs number entity description."""
|
||||
|
||||
@@ -94,7 +92,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EcovacsNumberEntity(
|
||||
class EcovacsNumberEntity[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]],
|
||||
NumberEntity,
|
||||
):
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic
|
||||
from typing import Any
|
||||
|
||||
from deebot_client.capabilities import CapabilitySetTypes
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.events import WorkModeEvent
|
||||
from deebot_client.events.base import Event
|
||||
from deebot_client.events.water_info import WaterAmountEvent
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
@@ -15,15 +16,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||
from .util import get_name_key, get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsSelectEntityDescription(
|
||||
class EcovacsSelectEntityDescription[EventT: Event](
|
||||
SelectEntityDescription,
|
||||
EcovacsCapabilityEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Ecovacs select entity description."""
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EcovacsSelectEntity(
|
||||
class EcovacsSelectEntity[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]],
|
||||
SelectEntity,
|
||||
):
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic
|
||||
from typing import Any
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
||||
from deebot_client.device import Device
|
||||
@@ -46,16 +46,14 @@ from .entity import (
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsEntity,
|
||||
EcovacsLegacyEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_name_key, get_options, get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsSensorEntityDescription(
|
||||
class EcovacsSensorEntityDescription[EventT: Event](
|
||||
EcovacsCapabilityEntityDescription,
|
||||
SensorEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Ecovacs sensor entity description."""
|
||||
|
||||
|
||||
@@ -285,9 +285,9 @@ class EsphomeAssistSatellite(
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||
data_to_send = {
|
||||
"tts_start_streaming": bool(
|
||||
event.data and event.data.get("tts_start_streaming")
|
||||
),
|
||||
"tts_start_streaming": "1"
|
||||
if (event.data and event.data.get("tts_start_streaming"))
|
||||
else "0",
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
||||
assert event.data is not None
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.2.4",
|
||||
"aioesphomeapi==33.0.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""The go2rtc component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from awesomeversion import AwesomeVersion
|
||||
from go2rtc_client import Go2RtcRestClient
|
||||
@@ -32,7 +35,7 @@ from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOM
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery_flow,
|
||||
@@ -98,6 +101,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
|
||||
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
|
||||
type Go2RtcConfigEntry = ConfigEntry[WebRTCProvider]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -151,13 +155,14 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
|
||||
"""Set up go2rtc from a config entry."""
|
||||
url = hass.data[_DATA_GO2RTC]
|
||||
|
||||
url = hass.data[_DATA_GO2RTC]
|
||||
session = async_get_clientsession(hass)
|
||||
client = Go2RtcRestClient(session, url)
|
||||
# Validate the server URL
|
||||
try:
|
||||
client = Go2RtcRestClient(async_get_clientsession(hass), url)
|
||||
version = await client.validate_server_version()
|
||||
if version < AwesomeVersion(RECOMMENDED_VERSION):
|
||||
ir.async_create_issue(
|
||||
@@ -188,13 +193,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
|
||||
provider = WebRTCProvider(hass, url)
|
||||
async_register_webrtc_provider(hass, provider)
|
||||
provider = entry.runtime_data = WebRTCProvider(hass, url, session, client)
|
||||
entry.async_on_unload(async_register_webrtc_provider(hass, provider))
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
|
||||
"""Unload a go2rtc config entry."""
|
||||
await entry.runtime_data.teardown()
|
||||
return True
|
||||
|
||||
|
||||
@@ -206,12 +212,18 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
|
||||
class WebRTCProvider(CameraWebRTCProvider):
|
||||
"""WebRTC provider."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, url: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
url: str,
|
||||
session: ClientSession,
|
||||
rest_client: Go2RtcRestClient,
|
||||
) -> None:
|
||||
"""Initialize the WebRTC provider."""
|
||||
self._hass = hass
|
||||
self._url = url
|
||||
self._session = async_get_clientsession(hass)
|
||||
self._rest_client = Go2RtcRestClient(self._session, url)
|
||||
self._session = session
|
||||
self._rest_client = rest_client
|
||||
self._sessions: dict[str, Go2RtcWsClient] = {}
|
||||
|
||||
@property
|
||||
@@ -232,32 +244,16 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
send_message: WebRTCSendMessage,
|
||||
) -> None:
|
||||
"""Handle the WebRTC offer and return the answer via the provided callback."""
|
||||
try:
|
||||
await self._update_stream_source(camera)
|
||||
except HomeAssistantError as err:
|
||||
send_message(WebRTCError("go2rtc_webrtc_offer_failed", str(err)))
|
||||
return
|
||||
|
||||
self._sessions[session_id] = ws_client = Go2RtcWsClient(
|
||||
self._session, self._url, source=camera.entity_id
|
||||
)
|
||||
|
||||
if not (stream_source := await camera.stream_source()):
|
||||
send_message(
|
||||
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
|
||||
)
|
||||
return
|
||||
|
||||
streams = await self._rest_client.streams.list()
|
||||
|
||||
if (stream := streams.get(camera.entity_id)) is None or not any(
|
||||
stream_source == producer.url for producer in stream.producers
|
||||
):
|
||||
await self._rest_client.streams.add(
|
||||
camera.entity_id,
|
||||
[
|
||||
stream_source,
|
||||
# We are setting any ffmpeg rtsp related logs to debug
|
||||
# Connection problems to the camera will be logged by the first stream
|
||||
# Therefore setting it to debug will not hide any important logs
|
||||
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
|
||||
],
|
||||
)
|
||||
|
||||
@callback
|
||||
def on_messages(message: ReceiveMessages) -> None:
|
||||
"""Handle messages."""
|
||||
@@ -291,3 +287,48 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
"""Close the session."""
|
||||
ws_client = self._sessions.pop(session_id)
|
||||
self._hass.async_create_task(ws_client.close())
|
||||
|
||||
async def async_get_image(
|
||||
self,
|
||||
camera: Camera,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> bytes | None:
|
||||
"""Get an image from the camera."""
|
||||
await self._update_stream_source(camera)
|
||||
return await self._rest_client.get_jpeg_snapshot(
|
||||
camera.entity_id, width, height
|
||||
)
|
||||
|
||||
async def _update_stream_source(self, camera: Camera) -> None:
|
||||
"""Update the stream source in go2rtc config if needed."""
|
||||
if not (stream_source := await camera.stream_source()):
|
||||
await self.teardown()
|
||||
raise HomeAssistantError("Camera has no stream source")
|
||||
|
||||
if not self.async_is_supported(stream_source):
|
||||
await self.teardown()
|
||||
raise HomeAssistantError("Stream source is not supported by go2rtc")
|
||||
|
||||
streams = await self._rest_client.streams.list()
|
||||
|
||||
if (stream := streams.get(camera.entity_id)) is None or not any(
|
||||
stream_source == producer.url for producer in stream.producers
|
||||
):
|
||||
await self._rest_client.streams.add(
|
||||
camera.entity_id,
|
||||
[
|
||||
stream_source,
|
||||
# We are setting any ffmpeg rtsp related logs to debug
|
||||
# Connection problems to the camera will be logged by the first stream
|
||||
# Therefore setting it to debug will not hide any important logs
|
||||
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
|
||||
f"ffmpeg:{camera.entity_id}#video=mjpeg",
|
||||
],
|
||||
)
|
||||
|
||||
async def teardown(self) -> None:
|
||||
"""Tear down the provider."""
|
||||
for ws_client in self._sessions.values():
|
||||
await ws_client.close()
|
||||
self._sessions.clear()
|
||||
|
||||
@@ -100,9 +100,11 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
try:
|
||||
response = await self._api.route(
|
||||
transport_mode=TransportMode(params.travel_mode),
|
||||
origin=here_routing.Place(params.origin[0], params.origin[1]),
|
||||
origin=here_routing.Place(
|
||||
float(params.origin[0]), float(params.origin[1])
|
||||
),
|
||||
destination=here_routing.Place(
|
||||
params.destination[0], params.destination[1]
|
||||
float(params.destination[0]), float(params.destination[1])
|
||||
),
|
||||
routing_mode=params.route_mode,
|
||||
arrival_time=params.arrival,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/here_travel_time",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"],
|
||||
"requirements": ["here-routing==1.0.1", "here-transit==1.2.1"]
|
||||
"requirements": ["here-routing==1.2.0", "here-transit==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
|
||||
from here_routing import RoutingMode
|
||||
|
||||
|
||||
class HERETravelTimeData(TypedDict):
|
||||
"""Routing information."""
|
||||
@@ -27,6 +29,6 @@ class HERETravelTimeAPIParams:
|
||||
destination: list[str]
|
||||
origin: list[str]
|
||||
travel_mode: str
|
||||
route_mode: str
|
||||
route_mode: RoutingMode
|
||||
arrival: datetime | None
|
||||
departure: datetime | None
|
||||
|
||||
@@ -22,6 +22,6 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohomeconnect==0.18.0"],
|
||||
"requirements": ["aiohomeconnect==0.18.1"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ class HomeeEntity(Entity):
|
||||
model=get_name_for_enum(NodeProfile, node.profile),
|
||||
via_device=(DOMAIN, entry.runtime_data.settings.uid),
|
||||
)
|
||||
if attribute.name:
|
||||
self._attr_name = attribute.name
|
||||
|
||||
self._host_connected = entry.runtime_data.connected
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["homee"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyHomee==1.2.9"]
|
||||
"requirements": ["pyHomee==1.2.10"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ import logging
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "homewizard"
|
||||
PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from homewizard_energy.errors import DisabledError, RequestError
|
||||
from homewizard_energy.errors import DisabledError, RequestError, UnauthorizedError
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
@@ -41,5 +41,10 @@ def homewizard_exception_handler[_HomeWizardEntityT: HomeWizardEntity, **_P](
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_disabled",
|
||||
) from ex
|
||||
except UnauthorizedError as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_unauthorized",
|
||||
) from ex
|
||||
|
||||
return handler
|
||||
|
||||
89
homeassistant/components/homewizard/select.py
Normal file
89
homeassistant/components/homewizard/select.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Support for HomeWizard select platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homewizard_energy import HomeWizardEnergy
|
||||
from homewizard_energy.models import Batteries, CombinedModels as DeviceResponseEntry
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
|
||||
from .entity import HomeWizardEntity
|
||||
from .helpers import homewizard_exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeWizardSelectEntityDescription(SelectEntityDescription):
|
||||
"""Class describing HomeWizard select entities."""
|
||||
|
||||
available_fn: Callable[[DeviceResponseEntry], bool]
|
||||
create_fn: Callable[[DeviceResponseEntry], bool]
|
||||
current_fn: Callable[[DeviceResponseEntry], str | None]
|
||||
set_fn: Callable[[HomeWizardEnergy, str], Awaitable[Any]]
|
||||
|
||||
|
||||
DESCRIPTIONS = [
|
||||
HomeWizardSelectEntityDescription(
|
||||
key="battery_group_mode",
|
||||
translation_key="battery_group_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
options=[Batteries.Mode.ZERO, Batteries.Mode.STANDBY, Batteries.Mode.TO_FULL],
|
||||
available_fn=lambda x: x.batteries is not None,
|
||||
create_fn=lambda x: x.batteries is not None,
|
||||
current_fn=lambda x: x.batteries.mode if x.batteries else None,
|
||||
set_fn=lambda api, mode: api.batteries(mode=Batteries.Mode(mode)),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeWizardConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up HomeWizard select based on a config entry."""
|
||||
async_add_entities(
|
||||
HomeWizardSelectEntity(
|
||||
coordinator=entry.runtime_data,
|
||||
description=description,
|
||||
)
|
||||
for description in DESCRIPTIONS
|
||||
if description.create_fn(entry.runtime_data.data)
|
||||
)
|
||||
|
||||
|
||||
class HomeWizardSelectEntity(HomeWizardEntity, SelectEntity):
|
||||
"""Defines a HomeWizard select entity."""
|
||||
|
||||
entity_description: HomeWizardSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HWEnergyDeviceUpdateCoordinator,
|
||||
description: HomeWizardSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
return self.entity_description.current_fn(self.coordinator.data)
|
||||
|
||||
@homewizard_exception_handler
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.entity_description.set_fn(self.coordinator.api, option)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -152,14 +152,27 @@
|
||||
"cloud_connection": {
|
||||
"name": "Cloud connection"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"battery_group_mode": {
|
||||
"name": "Battery group mode",
|
||||
"state": {
|
||||
"zero": "Zero mode",
|
||||
"to_full": "Manual charge mode",
|
||||
"standby": "Standby"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_disabled": {
|
||||
"message": "The local API is disabled."
|
||||
},
|
||||
"api_unauthorized": {
|
||||
"message": "The local API is unauthorized. Restore API access by following the instructions in the repair issue."
|
||||
},
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with HomeWizard device"
|
||||
"message": "An error occurred while communicating with your HomeWizard Energy device"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""The JuiceNet integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
@@ -14,9 +13,9 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .coordinator import JuiceNetCoordinator
|
||||
from .device import JuiceNetApi
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -74,20 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return False
|
||||
_LOGGER.debug("%d JuiceNet device(s) found", len(juicenet.devices))
|
||||
|
||||
async def async_update_data():
|
||||
"""Update all device states from the JuiceNet API."""
|
||||
for device in juicenet.devices:
|
||||
await device.update_state(True)
|
||||
return True
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="JuiceNet",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
coordinator = JuiceNetCoordinator(hass, entry, juicenet)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
33
homeassistant/components/juicenet/coordinator.py
Normal file
33
homeassistant/components/juicenet/coordinator.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""The JuiceNet integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .device import JuiceNetApi
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JuiceNetCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Coordinator for JuiceNet."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, juicenet_api: JuiceNetApi
|
||||
) -> None:
|
||||
"""Initialize the JuiceNet coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="JuiceNet",
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
self.juicenet_api = juicenet_api
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
for device in self.juicenet_api.devices:
|
||||
await device.update_state(True)
|
||||
@@ -1,19 +1,21 @@
|
||||
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
||||
|
||||
from pyjuicenet import Api, Charger
|
||||
|
||||
|
||||
class JuiceNetApi:
|
||||
"""Represent a connection to JuiceNet."""
|
||||
|
||||
def __init__(self, api):
|
||||
def __init__(self, api: Api) -> None:
|
||||
"""Create an object from the provided API instance."""
|
||||
self.api = api
|
||||
self._devices = []
|
||||
self._devices: list[Charger] = []
|
||||
|
||||
async def setup(self):
|
||||
async def setup(self) -> None:
|
||||
"""JuiceNet device setup."""
|
||||
self._devices = await self.api.get_devices()
|
||||
|
||||
@property
|
||||
def devices(self) -> list:
|
||||
def devices(self) -> list[Charger]:
|
||||
"""Get a list of devices managed by this account."""
|
||||
return self._devices
|
||||
|
||||
@@ -3,21 +3,19 @@
|
||||
from pyjuicenet import Charger
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JuiceNetCoordinator
|
||||
|
||||
|
||||
class JuiceNetDevice(CoordinatorEntity):
|
||||
class JuiceNetEntity(CoordinatorEntity[JuiceNetCoordinator]):
|
||||
"""Represent a base JuiceNet device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
|
||||
self, device: Charger, key: str, coordinator: JuiceNetCoordinator
|
||||
) -> None:
|
||||
"""Initialise the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyjuicenet import Api, Charger
|
||||
from pyjuicenet import Charger
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DEFAULT_MAX_VALUE,
|
||||
@@ -14,10 +14,11 @@ from homeassistant.components.number import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
from .coordinator import JuiceNetCoordinator
|
||||
from .device import JuiceNetApi
|
||||
from .entity import JuiceNetEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -47,8 +48,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the JuiceNet Numbers."""
|
||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api: Api = juicenet_data[JUICENET_API]
|
||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
api: JuiceNetApi = juicenet_data[JUICENET_API]
|
||||
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
|
||||
entities = [
|
||||
JuiceNetNumber(device, description, coordinator)
|
||||
@@ -58,7 +59,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetNumber(JuiceNetDevice, NumberEntity):
|
||||
class JuiceNetNumber(JuiceNetEntity, NumberEntity):
|
||||
"""Implementation of a JuiceNet number."""
|
||||
|
||||
entity_description: JuiceNetNumberEntityDescription
|
||||
@@ -67,7 +68,7 @@ class JuiceNetNumber(JuiceNetDevice, NumberEntity):
|
||||
self,
|
||||
device: Charger,
|
||||
description: JuiceNetNumberEntityDescription,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
coordinator: JuiceNetCoordinator,
|
||||
) -> None:
|
||||
"""Initialise the number."""
|
||||
super().__init__(device, description.key, coordinator)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyjuicenet import Charger
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -21,7 +23,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
from .coordinator import JuiceNetCoordinator
|
||||
from .device import JuiceNetApi
|
||||
from .entity import JuiceNetEntity
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@@ -74,8 +78,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the JuiceNet Sensors."""
|
||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = juicenet_data[JUICENET_API]
|
||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
api: JuiceNetApi = juicenet_data[JUICENET_API]
|
||||
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
|
||||
entities = [
|
||||
JuiceNetSensorDevice(device, coordinator, description)
|
||||
@@ -85,11 +89,14 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
|
||||
class JuiceNetSensorDevice(JuiceNetEntity, SensorEntity):
|
||||
"""Implementation of a JuiceNet sensor."""
|
||||
|
||||
def __init__(
|
||||
self, device, coordinator, description: SensorEntityDescription
|
||||
self,
|
||||
device: Charger,
|
||||
coordinator: JuiceNetCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialise the sensor."""
|
||||
super().__init__(device, description.key, coordinator)
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyjuicenet import Charger
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
from .coordinator import JuiceNetCoordinator
|
||||
from .device import JuiceNetApi
|
||||
from .entity import JuiceNetEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -18,20 +22,20 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the JuiceNet switches."""
|
||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = juicenet_data[JUICENET_API]
|
||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
api: JuiceNetApi = juicenet_data[JUICENET_API]
|
||||
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices
|
||||
)
|
||||
|
||||
|
||||
class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity):
|
||||
class JuiceNetChargeNowSwitch(JuiceNetEntity, SwitchEntity):
|
||||
"""Implementation of a JuiceNet switch."""
|
||||
|
||||
_attr_translation_key = "charge_now"
|
||||
|
||||
def __init__(self, device, coordinator):
|
||||
def __init__(self, device: Charger, coordinator: JuiceNetCoordinator) -> None:
|
||||
"""Initialise the switch."""
|
||||
super().__init__(device, "charge_now", coordinator)
|
||||
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import JustNimbusCoordinator
|
||||
from .const import PLATFORMS
|
||||
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||
"""Set up JustNimbus from a config entry."""
|
||||
if "zip_code" in entry.data:
|
||||
coordinator = JustNimbusCoordinator(hass, entry)
|
||||
@@ -18,13 +17,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryAuthFailed
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -16,13 +16,17 @@ from .const import CONF_ZIP_CODE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type JustNimbusConfigEntry = ConfigEntry[JustNimbusCoordinator]
|
||||
|
||||
|
||||
class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]):
|
||||
"""Data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: JustNimbusConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: JustNimbusConfigEntry
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
EntityCategory,
|
||||
@@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import JustNimbusCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||
from .entity import JustNimbusEntity
|
||||
|
||||
|
||||
@@ -102,16 +100,15 @@ SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: JustNimbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the JustNimbus sensor."""
|
||||
coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
JustNimbusSensor(
|
||||
device_id=entry.data[CONF_CLIENT_ID],
|
||||
description=description,
|
||||
coordinator=coordinator,
|
||||
coordinator=entry.runtime_data,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -3,26 +3,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
|
||||
|
||||
type KaleidescapeConfigEntry = ConfigEntry[KaleidescapeDevice]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: KaleidescapeConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Kaleidescape from a config entry."""
|
||||
device = KaleidescapeDevice(
|
||||
entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5
|
||||
@@ -36,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"Unable to connect to {entry.data[CONF_HOST]}: {err}"
|
||||
) from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device
|
||||
entry.runtime_data = device
|
||||
|
||||
async def disconnect(event: Event) -> None:
|
||||
await device.disconnect()
|
||||
@@ -44,18 +40,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
|
||||
)
|
||||
entry.async_on_unload(device.disconnect)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: KaleidescapeConfigEntry
|
||||
) -> bool:
|
||||
"""Unload config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await hass.data[DOMAIN][entry.entry_id].disconnect()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kaleidescape import const as kaleidescape_const
|
||||
|
||||
@@ -12,19 +12,13 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import KaleidescapeConfigEntry
|
||||
from .entity import KaleidescapeEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
|
||||
KALEIDESCAPE_PLAYING_STATES = [
|
||||
kaleidescape_const.PLAY_STATUS_PLAYING,
|
||||
kaleidescape_const.PLAY_STATUS_FORWARD,
|
||||
@@ -39,11 +33,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: KaleidescapeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the platform from a config entry."""
|
||||
entities = [KaleidescapeMediaPlayer(hass.data[DOMAIN][entry.entry_id])]
|
||||
entities = [KaleidescapeMediaPlayer(entry.runtime_data)]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -2,32 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from kaleidescape import const as kaleidescape_const
|
||||
|
||||
from homeassistant.components.remote import RemoteEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import KaleidescapeConfigEntry
|
||||
from .entity import KaleidescapeEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: KaleidescapeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the platform from a config entry."""
|
||||
entities = [KaleidescapeRemote(hass.data[DOMAIN][entry.entry_id])]
|
||||
entities = [KaleidescapeRemote(entry.runtime_data)]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -2,25 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kaleidescape import Device as KaleidescapeDevice
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import KaleidescapeConfigEntry
|
||||
from .entity import KaleidescapeEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from kaleidescape import Device as KaleidescapeDevice
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class KaleidescapeSensorEntityDescription(SensorEntityDescription):
|
||||
@@ -132,11 +127,11 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: KaleidescapeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the platform from a config entry."""
|
||||
device: KaleidescapeDevice = hass.data[DOMAIN][entry.entry_id]
|
||||
device = entry.runtime_data
|
||||
async_add_entities(
|
||||
KaleidescapeSensor(device, description) for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@@ -19,16 +18,14 @@ from .const import (
|
||||
DEFAULT_INTERFACE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
ROUTER,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from .router import KeeneticRouter
|
||||
from .router import KeeneticConfigEntry, KeeneticRouter
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KeeneticConfigEntry) -> bool:
|
||||
"""Set up the component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
async_add_defaults(hass, entry)
|
||||
@@ -36,32 +33,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
router = KeeneticRouter(hass, entry)
|
||||
await router.async_setup()
|
||||
|
||||
undo_listener = entry.add_update_listener(update_listener)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
ROUTER: router,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
entry.runtime_data = router
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: KeeneticConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
|
||||
router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
|
||||
|
||||
router = config_entry.runtime_data
|
||||
await router.async_teardown()
|
||||
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
new_tracked_interfaces: set[str] = set(config_entry.options[CONF_INTERFACES])
|
||||
|
||||
if router.tracked_interfaces - new_tracked_interfaces:
|
||||
@@ -96,12 +87,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def update_listener(hass: HomeAssistant, entry: KeeneticConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
def async_add_defaults(hass: HomeAssistant, entry: ConfigEntry):
|
||||
def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry):
|
||||
"""Populate default options."""
|
||||
host: str = entry.data[CONF_HOST]
|
||||
imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {})
|
||||
|
||||
@@ -4,24 +4,20 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import KeeneticRouter
|
||||
from .const import DOMAIN, ROUTER
|
||||
from .router import KeeneticConfigEntry, KeeneticRouter
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: KeeneticConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up device tracker for Keenetic NDMS2 component."""
|
||||
router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
|
||||
|
||||
async_add_entities([RouterOnlineBinarySensor(router)])
|
||||
async_add_entities([RouterOnlineBinarySensor(config_entry.runtime_data)])
|
||||
|
||||
|
||||
class RouterOnlineBinarySensor(BinarySensorEntity):
|
||||
|
||||
@@ -8,12 +8,7 @@ from urllib.parse import urlparse
|
||||
from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -41,9 +36,8 @@ from .const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEFAULT_TELNET_PORT,
|
||||
DOMAIN,
|
||||
ROUTER,
|
||||
)
|
||||
from .router import KeeneticRouter
|
||||
from .router import KeeneticConfigEntry
|
||||
|
||||
|
||||
class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -56,7 +50,7 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: KeeneticConfigEntry,
|
||||
) -> KeeneticOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return KeeneticOptionsFlowHandler()
|
||||
@@ -142,6 +136,8 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class KeeneticOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle options."""
|
||||
|
||||
config_entry: KeeneticConfigEntry
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize options flow."""
|
||||
self._interface_options: dict[str, str] = {}
|
||||
@@ -150,9 +146,7 @@ class KeeneticOptionsFlowHandler(OptionsFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][
|
||||
ROUTER
|
||||
]
|
||||
router = self.config_entry.runtime_data
|
||||
|
||||
interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
|
||||
router.client.get_interfaces
|
||||
|
||||
@@ -5,8 +5,6 @@ from homeassistant.components.device_tracker import (
|
||||
)
|
||||
|
||||
DOMAIN = "keenetic_ndms2"
|
||||
ROUTER = "router"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
DEFAULT_TELNET_PORT = 23
|
||||
DEFAULT_SCAN_INTERVAL = 120
|
||||
DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds()
|
||||
|
||||
@@ -10,26 +10,24 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
||||
ScannerEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, ROUTER
|
||||
from .router import KeeneticRouter
|
||||
from .router import KeeneticConfigEntry, KeeneticRouter
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: KeeneticConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up device tracker for Keenetic NDMS2 component."""
|
||||
router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
|
||||
router = config_entry.runtime_data
|
||||
|
||||
tracked: set[str] = set()
|
||||
|
||||
|
||||
@@ -35,11 +35,13 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type KeeneticConfigEntry = ConfigEntry[KeeneticRouter]
|
||||
|
||||
|
||||
class KeeneticRouter:
|
||||
"""Keenetic client Object."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config_entry: KeeneticConfigEntry) -> None:
|
||||
"""Initialize the Client."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
|
||||
@@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type KegtronConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool:
|
||||
"""Set up Kegtron BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = KegtronBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
@@ -42,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,11 +8,9 @@ from kegtron_ble import (
|
||||
Units,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -30,7 +28,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import KegtronConfigEntry
|
||||
from .device import device_key_to_bluetooth_entity_key
|
||||
|
||||
SENSOR_DESCRIPTIONS = {
|
||||
@@ -109,13 +107,11 @@ def sensor_update_to_bluetooth_data_update(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: KegtronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Kegtron BLE sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
|
||||
@@ -2,26 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from microbot import MicroBotApiClient
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MicroBotDataUpdateCoordinator
|
||||
from .coordinator import MicroBotConfigEntry, MicroBotDataUpdateCoordinator
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
token: str = entry.data[CONF_ACCESS_TOKEN]
|
||||
bdaddr: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr)
|
||||
@@ -35,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, client=client, ble_device=ble_device
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
@@ -43,9 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -11,14 +11,15 @@ from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||
|
||||
type MicroBotConfigEntry = ConfigEntry[MicroBotDataUpdateCoordinator]
|
||||
|
||||
|
||||
class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
@@ -31,7 +32,7 @@ class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
ble_device: BLEDevice,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.api: MicroBotApiClient = client
|
||||
self.api = client
|
||||
self.data: dict[str, Any] = {}
|
||||
self.ble_device = ble_device
|
||||
super().__init__(
|
||||
|
||||
@@ -19,7 +19,7 @@ class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordin
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator, config_entry):
|
||||
def __init__(self, coordinator: MicroBotDataUpdateCoordinator) -> None:
|
||||
"""Initialise the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._address = self.coordinator.ble_device.address
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -16,8 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MicroBotDataUpdateCoordinator
|
||||
from .coordinator import MicroBotConfigEntry
|
||||
from .entity import MicroBotEntity
|
||||
|
||||
CALIBRATE = "calibrate"
|
||||
@@ -30,12 +28,11 @@ CALIBRATE_SCHEMA: VolDictType = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MicroBot based on a config entry."""
|
||||
coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([MicroBotBinarySwitch(coordinator, entry)])
|
||||
async_add_entities([MicroBotBinarySwitch(entry.runtime_data)])
|
||||
platform = async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
CALIBRATE,
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
"""The kmtronic integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from pykmtronic.auth import Auth
|
||||
from pykmtronic.hub import KMTronicHubAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER
|
||||
from .coordinator import KMTronicConfigEntry, KMtronicCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SWITCH]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool:
|
||||
"""Set up kmtronic from a config entry."""
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
auth = Auth(
|
||||
@@ -31,51 +22,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
hub = KMTronicHubAPI(auth)
|
||||
|
||||
async def async_update_data():
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await hub.async_update_relays()
|
||||
except aiohttp.client_exceptions.ClientResponseError as err:
|
||||
raise UpdateFailed(f"Wrong credentials: {err}") from err
|
||||
except aiohttp.client_exceptions.ClientConnectorError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{MANUFACTURER} {hub.name}",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
coordinator = KMtronicCoordinator(hass, entry, hub)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_HUB: hub,
|
||||
DATA_COORDINATOR: coordinator,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
update_listener = entry.add_update_listener(async_update_options)
|
||||
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
async def async_update_options(
|
||||
hass: HomeAssistant, config_entry: KMTronicConfigEntry
|
||||
) -> None:
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
|
||||
update_listener()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -4,9 +4,4 @@ DOMAIN = "kmtronic"
|
||||
|
||||
CONF_REVERSE = "reverse"
|
||||
|
||||
DATA_HUB = "hub"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
|
||||
MANUFACTURER = "KMtronic"
|
||||
|
||||
UPDATE_LISTENER = "update_listener"
|
||||
|
||||
50
homeassistant/components/kmtronic/coordinator.py
Normal file
50
homeassistant/components/kmtronic/coordinator.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""The kmtronic integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError
|
||||
from pykmtronic.hub import KMTronicHubAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import MANUFACTURER
|
||||
|
||||
PLATFORMS = [Platform.SWITCH]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type KMTronicConfigEntry = ConfigEntry[KMtronicCoordinator]
|
||||
|
||||
|
||||
class KMtronicCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Coordinator for KMTronic."""
|
||||
|
||||
entry: KMTronicConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: KMTronicConfigEntry, hub: KMTronicHubAPI
|
||||
) -> None:
|
||||
"""Initialize the KMTronic coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{MANUFACTURER} {hub.name}",
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
self.hub = hub
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch the latest data from the source."""
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await self.hub.async_update_relays()
|
||||
except ClientResponseError as err:
|
||||
raise UpdateFailed(f"Wrong credentials: {err}") from err
|
||||
except ClientConnectorError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
@@ -4,23 +4,23 @@ from typing import Any
|
||||
import urllib.parse
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER
|
||||
from .const import CONF_REVERSE, DOMAIN, MANUFACTURER
|
||||
from .coordinator import KMTronicConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: KMTronicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Config entry example."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
|
||||
hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB]
|
||||
coordinator = entry.runtime_data
|
||||
hub = coordinator.hub
|
||||
reverse = entry.options.get(CONF_REVERSE, False)
|
||||
await hub.async_get_relays()
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""The kodi component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection
|
||||
from pykodi.kodi import KodiHTTPConnection, KodiWSConnection
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -17,19 +19,23 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_WS_PORT,
|
||||
DATA_CONNECTION,
|
||||
DATA_KODI,
|
||||
DATA_REMOVE_LISTENER,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_WS_PORT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
type KodiConfigEntry = ConfigEntry[KodiRuntimeData]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@dataclass
|
||||
class KodiRuntimeData:
|
||||
"""Data class to hold Kodi runtime data."""
|
||||
|
||||
connection: KodiHTTPConnection | KodiWSConnection
|
||||
kodi: Kodi
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KodiConfigEntry) -> bool:
|
||||
"""Set up Kodi from a config entry."""
|
||||
conn = get_kodi_connection(
|
||||
entry.data[CONF_HOST],
|
||||
@@ -58,26 +64,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def _close(event):
|
||||
await conn.close()
|
||||
|
||||
remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)
|
||||
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_CONNECTION: conn,
|
||||
DATA_KODI: kodi,
|
||||
DATA_REMOVE_LISTENER: remove_stop_listener,
|
||||
}
|
||||
entry.runtime_data = KodiRuntimeData(connection=conn, kodi=kodi)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KodiConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
data = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await data[DATA_CONNECTION].close()
|
||||
data[DATA_REMOVE_LISTENER]()
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await entry.runtime_data.connection.close()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -4,10 +4,6 @@ DOMAIN = "kodi"
|
||||
|
||||
CONF_WS_PORT = "ws_port"
|
||||
|
||||
DATA_CONNECTION = "connection"
|
||||
DATA_KODI = "kodi"
|
||||
DATA_REMOVE_LISTENER = "remove_listener"
|
||||
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -55,6 +55,7 @@ from homeassistant.helpers.network import is_internal_request
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import KodiConfigEntry
|
||||
from .browse_media import (
|
||||
build_item_response,
|
||||
get_media_info,
|
||||
@@ -63,8 +64,6 @@ from .browse_media import (
|
||||
)
|
||||
from .const import (
|
||||
CONF_WS_PORT,
|
||||
DATA_CONNECTION,
|
||||
DATA_KODI,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_TIMEOUT,
|
||||
@@ -208,7 +207,7 @@ async def async_setup_platform(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: KodiConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Kodi media player platform."""
|
||||
@@ -220,14 +219,12 @@ async def async_setup_entry(
|
||||
SERVICE_CALL_METHOD, KODI_CALL_METHOD_SCHEMA, "async_call_method"
|
||||
)
|
||||
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
connection = data[DATA_CONNECTION]
|
||||
kodi = data[DATA_KODI]
|
||||
data = config_entry.runtime_data
|
||||
name = config_entry.data[CONF_NAME]
|
||||
if (uid := config_entry.unique_id) is None:
|
||||
uid = config_entry.entry_id
|
||||
|
||||
entity = KodiEntity(connection, kodi, name, uid)
|
||||
entity = KodiEntity(data.connection, data.kodi, name, uid)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ from .const import (
|
||||
PIN_TO_ZONE,
|
||||
STATE_HIGH,
|
||||
STATE_LOW,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
UPDATE_ENDPOINT,
|
||||
ZONE_TO_PIN,
|
||||
ZONES,
|
||||
@@ -261,10 +260,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# config entry specific data to enable unload
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: entry.add_update_listener(async_entry_updated)
|
||||
}
|
||||
entry.async_on_unload(entry.add_update_listener(async_entry_updated))
|
||||
return True
|
||||
|
||||
|
||||
@@ -272,11 +268,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][CONF_DEVICES].pop(entry.data[CONF_ID])
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -44,5 +44,3 @@ ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()}
|
||||
ENDPOINT_ROOT = "/api/konnected"
|
||||
UPDATE_ENDPOINT = ENDPOINT_ROOT + r"/device/{device_id:[a-zA-Z0-9]+}"
|
||||
SIGNAL_DS18B20_NEW = "konnected.ds18b20.new"
|
||||
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
@@ -4,42 +4,35 @@ import logging
|
||||
|
||||
from pykoplenti import ApiException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Plenticore
|
||||
from .coordinator import Plenticore, PlenticoreConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PlenticoreConfigEntry) -> bool:
|
||||
"""Set up Kostal Plenticore Solar Inverter from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
plenticore = Plenticore(hass, entry)
|
||||
|
||||
if not await plenticore.async_setup():
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = plenticore
|
||||
entry.runtime_data = plenticore
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PlenticoreConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
# remove API object
|
||||
plenticore = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
try:
|
||||
await plenticore.async_unload()
|
||||
await entry.runtime_data.async_unload()
|
||||
except ApiException as err:
|
||||
_LOGGER.error("Error logging out from inverter: %s", err)
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ from .helper import get_hostname_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type PlenticoreConfigEntry = ConfigEntry[Plenticore]
|
||||
|
||||
|
||||
class Plenticore:
|
||||
"""Manages the Plenticore API."""
|
||||
@@ -166,12 +168,12 @@ class DataUpdateCoordinatorMixin:
|
||||
class PlenticoreUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Base implementation of DataUpdateCoordinator for Plenticore data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PlenticoreConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PlenticoreConfigEntry,
|
||||
logger: logging.Logger,
|
||||
name: str,
|
||||
update_inverval: timedelta,
|
||||
@@ -248,12 +250,12 @@ class SettingDataUpdateCoordinator(
|
||||
class PlenticoreSelectUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Base implementation of DataUpdateCoordinator for Plenticore data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PlenticoreConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PlenticoreConfigEntry,
|
||||
logger: logging.Logger,
|
||||
name: str,
|
||||
update_inverval: timedelta,
|
||||
|
||||
@@ -5,23 +5,21 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED, async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_IDENTIFIERS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Plenticore
|
||||
from .coordinator import PlenticoreConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: PlenticoreConfigEntry
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)}
|
||||
|
||||
plenticore: Plenticore = hass.data[DOMAIN][config_entry.entry_id]
|
||||
plenticore = config_entry.runtime_data
|
||||
|
||||
# Get information from Kostal Plenticore library
|
||||
available_process_data = await plenticore.client.get_process_data()
|
||||
|
||||
@@ -14,15 +14,13 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SettingDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator
|
||||
from .helper import PlenticoreDataFormatter
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -74,11 +72,11 @@ NUMBER_SETTINGS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Kostal Plenticore Number entities."""
|
||||
plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
|
||||
@@ -7,15 +7,13 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Plenticore, SelectDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, SelectDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,11 +41,11 @@ SELECT_SETTINGS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add kostal plenticore Select widget."""
|
||||
plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
available_settings_data = await plenticore.client.get_settings()
|
||||
select_data_update_coordinator = SelectDataUpdateCoordinator(
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
@@ -29,8 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ProcessDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, ProcessDataUpdateCoordinator
|
||||
from .helper import PlenticoreDataFormatter
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -808,11 +806,11 @@ SENSOR_PROCESS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add kostal plenticore Sensors."""
|
||||
plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
|
||||
@@ -8,15 +8,13 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SettingDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,11 +47,11 @@ SWITCH_SETTINGS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add kostal plenticore Switch."""
|
||||
plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
|
||||
@@ -6,20 +6,18 @@ import logging
|
||||
|
||||
from lacrosse_view import LaCrosse, LoginError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LaCrosseUpdateCoordinator
|
||||
from .coordinator import LaCrosseConfigEntry, LaCrosseUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LaCrosseConfigEntry) -> bool:
|
||||
"""Set up LaCrosse View from a config entry."""
|
||||
|
||||
api = LaCrosse(async_get_clientsession(hass))
|
||||
@@ -35,9 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.debug("First refresh")
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
"coordinator": coordinator,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
_LOGGER.debug("Setting up platforms")
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
@@ -45,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LaCrosseConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -17,6 +17,8 @@ from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LaCrosseConfigEntry = ConfigEntry[LaCrosseUpdateCoordinator]
|
||||
|
||||
|
||||
class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
"""DataUpdateCoordinator for LaCrosse View."""
|
||||
@@ -27,12 +29,12 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
id: str
|
||||
hass: HomeAssistant
|
||||
devices: list[Sensor] | None = None
|
||||
config_entry: ConfigEntry
|
||||
config_entry: LaCrosseConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: LaCrosseConfigEntry,
|
||||
api: LaCrosse,
|
||||
) -> None:
|
||||
"""Initialize DataUpdateCoordinator for LaCrosse View."""
|
||||
|
||||
@@ -5,25 +5,20 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LaCrosseUpdateCoordinator
|
||||
from .coordinator import LaCrosseConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: LaCrosseConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: LaCrosseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||
"coordinator"
|
||||
]
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"coordinator_data": coordinator.data,
|
||||
"coordinator_data": entry.runtime_data.data,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
@@ -32,6 +31,7 @@ from homeassistant.helpers.update_coordinator import (
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LaCrosseConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -159,17 +159,14 @@ UNIT_OF_MEASUREMENT_MAP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: LaCrosseConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LaCrosse View from a config entry."""
|
||||
coordinator: DataUpdateCoordinator[list[Sensor]] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]["coordinator"]
|
||||
sensors: list[Sensor] = coordinator.data
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
sensor_list = []
|
||||
for i, sensor in enumerate(sensors):
|
||||
for i, sensor in enumerate(coordinator.data):
|
||||
for field in sensor.sensor_field_names:
|
||||
description = SENSOR_DESCRIPTIONS.get(field)
|
||||
if description is None:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"requirements": ["pypck==0.8.8", "lcn-frontend==0.2.5"]
|
||||
"requirements": ["pypck==0.8.9", "lcn-frontend==0.2.5"]
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ ATTR_ACTION = "action"
|
||||
ATTR_FULL_ID = "full_id"
|
||||
ATTR_UUID = "uuid"
|
||||
|
||||
type LutronConfigEntry = ConfigEntry[LutronData]
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class LutronData:
|
||||
@@ -44,7 +46,9 @@ class LutronData:
|
||||
switches: list[tuple[str, Output]]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: LutronConfigEntry
|
||||
) -> bool:
|
||||
"""Set up the Lutron integration."""
|
||||
|
||||
host = config_entry.data[CONF_HOST]
|
||||
@@ -169,7 +173,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
name="Main repeater",
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = entry_data
|
||||
config_entry.runtime_data = entry_data
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@@ -222,6 +226,6 @@ def _async_check_device_identifiers(
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LutronConfigEntry) -> bool:
|
||||
"""Clean up resources and entities associated with the integration."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pylutron import OccupancyGroup
|
||||
@@ -12,19 +11,16 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, LutronData
|
||||
from . import LutronConfigEntry
|
||||
from .entity import LutronDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron binary_sensor platform.
|
||||
@@ -32,7 +28,7 @@ async def async_setup_entry(
|
||||
Adds occupancy groups from the Main Repeater associated with the
|
||||
config_entry as binary_sensor entities.
|
||||
"""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
[
|
||||
LutronOccupancySensor(area_name, device, entry_data.client)
|
||||
|
||||
@@ -9,12 +9,7 @@ from urllib.error import HTTPError
|
||||
from pylutron import Lutron
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -23,6 +18,7 @@ from homeassistant.helpers.selector import (
|
||||
NumberSelectorMode,
|
||||
)
|
||||
|
||||
from . import LutronConfigEntry
|
||||
from .const import CONF_DEFAULT_DIMMER_LEVEL, DEFAULT_DIMMER_LEVEL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -83,7 +79,7 @@ class LutronConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
@@ -13,11 +13,10 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, LutronData
|
||||
from . import LutronConfigEntry
|
||||
from .entity import LutronDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -25,7 +24,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron cover platform.
|
||||
@@ -33,7 +32,7 @@ async def async_setup_entry(
|
||||
Adds shades from the Main Repeater associated with the config_entry as
|
||||
cover entities.
|
||||
"""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
[
|
||||
LutronCover(area_name, device, entry_data.client)
|
||||
|
||||
@@ -5,13 +5,12 @@ from enum import StrEnum
|
||||
from pylutron import Button, Keypad, Lutron, LutronEvent
|
||||
|
||||
from homeassistant.components.event import EventEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, DOMAIN, LutronData
|
||||
from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, LutronConfigEntry
|
||||
from .entity import LutronKeypad
|
||||
|
||||
|
||||
@@ -32,11 +31,11 @@ LEGACY_EVENT_TYPES: dict[LutronEventType, str] = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron event platform."""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
LutronEventEntity(area_name, keypad, button, entry_data.client)
|
||||
|
||||
@@ -2,25 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pylutron import Output
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, LutronData
|
||||
from . import LutronConfigEntry
|
||||
from .entity import LutronDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron fan platform.
|
||||
@@ -28,7 +24,7 @@ async def async_setup_entry(
|
||||
Adds fan controls from the Main Repeater associated with the config_entry as
|
||||
fan entities.
|
||||
"""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
[
|
||||
LutronFan(area_name, device, entry_data.client)
|
||||
|
||||
@@ -19,14 +19,14 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, LutronData
|
||||
from . import LutronConfigEntry
|
||||
from .const import CONF_DEFAULT_DIMMER_LEVEL, DEFAULT_DIMMER_LEVEL
|
||||
from .entity import LutronDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron light platform.
|
||||
@@ -34,7 +34,7 @@ async def async_setup_entry(
|
||||
Adds dimmers from the Main Repeater associated with the config_entry as
|
||||
light entities.
|
||||
"""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
|
||||
@@ -7,17 +7,16 @@ from typing import Any
|
||||
from pylutron import Button, Keypad, Lutron
|
||||
|
||||
from homeassistant.components.scene import Scene
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, LutronData
|
||||
from . import LutronConfigEntry
|
||||
from .entity import LutronKeypad
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron scene platform.
|
||||
@@ -25,7 +24,7 @@ async def async_setup_entry(
|
||||
Adds scenes from the Main Repeater associated with the config_entry as
|
||||
scene entities.
|
||||
"""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
LutronScene(area_name, keypad, device, entry_data.client)
|
||||
|
||||
@@ -8,17 +8,16 @@ from typing import Any
|
||||
from pylutron import Button, Keypad, Led, Lutron, Output
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, LutronData
|
||||
from . import LutronConfigEntry
|
||||
from .entity import LutronDevice, LutronKeypad
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: LutronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lutron switch platform.
|
||||
@@ -26,7 +25,7 @@ async def async_setup_entry(
|
||||
Adds switches from the Main Repeater associated with the config_entry as
|
||||
switch entities.
|
||||
"""
|
||||
entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entry_data = config_entry.runtime_data
|
||||
entities: list[SwitchEntity] = []
|
||||
|
||||
# Add Lutron Switches
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
"bat_replacement_description": {
|
||||
"default": "mdi:battery-sync"
|
||||
},
|
||||
"battery_voltage": {
|
||||
"default": "mdi:current-dc"
|
||||
},
|
||||
"flow": {
|
||||
"default": "mdi:pipe"
|
||||
},
|
||||
|
||||
@@ -345,6 +345,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="PowerSourceBatVoltage",
|
||||
translation_key="battery_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
|
||||
@@ -324,6 +324,9 @@
|
||||
"battery_replacement_description": {
|
||||
"name": "Battery type"
|
||||
},
|
||||
"battery_voltage": {
|
||||
"name": "Battery voltage"
|
||||
},
|
||||
"current_phase": {
|
||||
"name": "Current phase"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user