Compare commits

...

50 Commits

Author SHA1 Message Date
J. Nick Koston
a4efd31b7d Bump aioesphomeapi to 33.0.0
fixes compat warning with protobuf 6.x

changelog: https://github.com/esphome/aioesphomeapi/compare/v32.2.4...v33.0.0

Not a breaking change for HA since we are already on protobuf 6
2025-06-22 11:49:15 +02:00
G Johansson
db3090078b Remove deprecated support feature values in camera (#146988) 2025-06-22 09:31:16 +02:00
Ludovic BOUÉ
66e2fd997b Battery voltage translation key (#147238)
* Add translation_key

* Update strings.json

* Update snapshots

* Switch icon to DC

* Update snapshots
2025-06-22 09:27:44 +02:00
Raphael Hehl
a102eaf0cd Bump uiprotect to version 7.14.1 (#147280) 2025-06-22 02:14:26 +02:00
Shai Ungar
f3533dff44 Bump pyseventeentrack to 1.1.1 (#147253)
Update pyseventeentrack requirement to version 1.1.1
2025-06-21 22:50:53 +01:00
Simone Chemelli
c453eed32d Bump aioamazondevices to 3.1.14 (#147257) 2025-06-21 15:44:22 +02:00
Joakim Sørensen
79a9f34150 Handle the new JSON payload from traccar clients (#147254) 2025-06-21 11:53:17 +02:00
hanwg
7442f7af28 Fix Telegram bot parsing of inline keyboard (#146376)
* bug fix for inline keyboard

* update inline keyboard test

* Update tests/components/telegram_bot/test_telegram_bot.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* revert last_message_id and updated tests

* removed TypeError test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-21 03:21:10 +02:00
Markus Adrario
2e5de732a7 Bump pyHomee to version 1.2.10 (#147248)
bump pyHomee to version 1.2.10
2025-06-21 00:32:14 +01:00
Michael Hansen
9bcd74c449 Change async_supports_streaming_input to an instance method (#147245) 2025-06-20 15:39:22 -05:00
Noah Husby
ace18e540b Bump aiorussound to 4.6.1 (#147233) 2025-06-20 21:59:59 +02:00
Michael Hansen
65f897793d Use string instead of boolean for voice event (#147244)
Use string instead of bool
2025-06-20 15:18:03 -04:00
Robert Resch
435c08685d Bump deebot-client to 13.4.0 (#147221) 2025-06-20 20:22:33 +02:00
J. Diego Rodríguez Royo
95f292c43d Bump aiohomeconnect to 0.18.1 (#147236) 2025-06-20 19:27:29 +02:00
Manu
9346c584c3 Add reconfigure flow to ntfy integration (#143743) 2025-06-20 18:42:47 +02:00
Michael Hansen
6738085391 Minor clean up missed in previous PR (#147229) 2025-06-20 10:54:11 -05:00
Markus Adrario
d9e5bad55e Use entity name in homee (#147142)
* add name to HomeeEntity

* review change
2025-06-20 16:55:48 +02:00
Maciej Bieniek
f7429f3431 Fix Shelly entity names for gen1 sleeping devices (#147019) 2025-06-20 15:19:39 +02:00
Petar Petrov
46aea5d9dc Bump zwave-js-server-python to 0.64.0 (#147176) 2025-06-20 14:59:54 +02:00
Paulus Schoutsen
33bde48c9c AI Task integration (#145128)
* Add AI Task integration

* Remove GenTextTaskType

* Add AI Task prefs

* Add action to LLM task

* Remove WS command

* Rename result to text for GenTextTaskResult

* Apply suggestions from code review

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* Add supported feature for generate text

* Update const.py

Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com>

* Update homeassistant/components/ai_task/services.yaml

Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com>

* Use WS API to set preferences

* Simplify pref storage

* Simplify pref test

* Update homeassistant/components/ai_task/services.yaml

Co-authored-by: Allen Porter <allen.porter@gmail.com>

---------

Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com>
2025-06-20 08:56:08 -04:00
Noah Husby
1b73acc025 Add sub-device support to Russound RIO (#146763) 2025-06-20 14:52:34 +02:00
Guido Schmitz
e28965770e Add translations for devolo Home Control exceptions (#147099)
* Add translations for devolo Home Control exceptions

* Adapt invalid_auth message

* Adapt connection_failed message
2025-06-20 14:31:16 +02:00
Kevin Stillhammer
f9d4bde0f6 Bump here-routing to 1.2.0 (#147204)
* Bump here-routing to 1.2.0

* Fix mypy typing errors

* Correct types for call assertion
2025-06-20 13:44:14 +02:00
Duco Sebel
a493bdc208 Implement battery group mode in HomeWizard (#146770)
* Implement battery group mode for HomeWizard P1

* Clean up test

* Disable 'entity_registry_enabled_default'

* Fix failing tests because of 'entity_registry_enabled_default'

* Proof entities are disabled by default

* Undo dev change

* Update homeassistant/components/homewizard/select.py

* Update homeassistant/components/homewizard/select.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/homewizard/strings.json

* Apply suggestions from code review

* Update tests due to updated translations

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-20 13:19:45 +02:00
Markus Adrario
9ae9ad1e43 Improve test-coverage for homee locks (#147160)
test for unknown user
2025-06-20 12:28:49 +02:00
epenet
1b60ea8951 Migrate lutron to use runtime_data (#147198) 2025-06-20 12:26:07 +02:00
epenet
313eaff14e Migrate kaleidescape to use runtime_data (#147171)
* Migrate kaleidescape to use runtime_data

* Adjust tests
2025-06-20 12:25:57 +02:00
epenet
7dfd68f8c0 Migrate keenetic_ndms2 to use runtime_data (#147194)
* Migrate keenetic_ndms2 to use runtime_data

* Adjust tests
2025-06-20 12:23:59 +02:00
epenet
544fd2a4a6 Migrate lacrosse_view to use runtime_data (#147202) 2025-06-20 12:23:29 +02:00
epenet
cd51070219 Migrate kmtronic to use runtime_data (#147193) 2025-06-20 11:39:13 +02:00
Marc Mueller
3c91c78383 Use PEP 695 TypeVar syntax for ecovacs (#147153) 2025-06-20 10:41:25 +02:00
Brett Adams
96e0d1f5c6 Fix Charge Cable binary sensor in Teslemetry (#147136) 2025-06-20 10:39:43 +02:00
epenet
2859e7de9b Migrate kodi to use runtime_data (#147191) 2025-06-20 10:38:01 +02:00
Robert Resch
88683a318d Add support of taking a camera snapshot via go2rtc (#145205) 2025-06-20 10:34:43 +02:00
epenet
84e9422254 Move juicenet coordinator to separate module (#147168) 2025-06-20 10:33:17 +02:00
epenet
fde36d5034 Simplify update_listener in konnected (#147172) 2025-06-20 10:31:28 +02:00
Andre Lengwenus
8c1e43c07c Bump pypck to 0.8.9 (#147174) 2025-06-20 10:28:35 +02:00
epenet
05343392a7 Simplify update_listener in keenetic_ndms2 (#147173) 2025-06-20 10:27:47 +02:00
epenet
32314dbb13 Simplify update_listener in kmtronic (#147184) 2025-06-20 10:27:07 +02:00
epenet
8f661fc5cf Migrate kegtron to use runtime_data (#147177) 2025-06-20 10:26:53 +02:00
epenet
e315cb9859 Migrate kostal_plenticore to use runtime_data (#147188) 2025-06-20 10:25:08 +02:00
epenet
d0e77eb1e2 Migrate keymitt_ble to use runtime_data (#147179) 2025-06-20 10:24:56 +02:00
epenet
e23cac8bef Simplify remove listener in kodi (#147183) 2025-06-20 10:23:41 +02:00
epenet
973700542b Move kmtronic coordinator to separate module (#147182) 2025-06-20 10:19:19 +02:00
Krisjanis Lejejs
2e21493c19 Bump hass-nabucasa from 0.102.0 to 0.103.0 (#147186) 2025-06-20 10:18:03 +02:00
Markus Adrario
73bed96a0f remove unwanted attribute in homee sensor tests (#147158) 2025-06-20 08:11:20 +02:00
Markus Adrario
0a5d13f104 fix and improve cover tests for homee (#147164) 2025-06-20 08:10:44 +02:00
epenet
d16ec81727 Migrate justnimbus to use runtime_data (#147170) 2025-06-20 08:10:06 +02:00
Martin Hjelmare
11564e3df5 Fix Z-Wave device class endpoint discovery (#142171)
* Add test fixture and test for Glass 9 shutter

* Fix zwave_js device class discovery matcher

* Fall back to node device class

* Fix test_special_meters modifying node state

* Handle value added after node ready
2025-06-20 08:56:20 +03:00
Michael Hansen
341d9f15f0 Add ask_question action to Assist satellite (#145233)
* Add get_response to Assist satellite and ESPHome

* Rename get_response to ask_question

* Add possible answers to questions

* Add wildcard support and entity test

* Add ESPHome test

* Refactor to remove async_ask_question

* Use single entity_id instead of target

* Fix error message

* Remove ESPHome test

* Clean up

* Revert fix
2025-06-19 16:50:14 -05:00
181 changed files with 12367 additions and 1194 deletions

2
CODEOWNERS generated
View File

@@ -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

View 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}

View 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."""

View 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

View 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())

View File

@@ -0,0 +1,7 @@
{
"services": {
"generate_text": {
"service": "mdi:file-star-four-points-outline"
}
}
}

View 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"
}

View 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

View 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."
}
}
}
}
}

View 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,
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.12"]
"requirements": ["aioamazondevices==3.1.14"]
}

View File

@@ -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

View File

@@ -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)

View File

@@ -10,6 +10,9 @@
},
"start_conversation": {
"service": "mdi:forum"
},
"ask_question": {
"service": "mdi:microphone-question"
}
}
}

View File

@@ -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"]
}

View File

@@ -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:

View File

@@ -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."
}
}
}
}
}

View File

@@ -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()

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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."
}
}
}

View File

@@ -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,
):

View File

@@ -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):

View File

@@ -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"]
}

View File

@@ -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,
):

View File

@@ -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,
):

View File

@@ -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."""

View File

@@ -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

View File

@@ -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"
],

View File

@@ -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()

View File

@@ -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,

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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."]
}

View File

@@ -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

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["homee"],
"quality_scale": "bronze",
"requirements": ["pyHomee==1.2.9"]
"requirements": ["pyHomee==1.2.10"]
}

View File

@@ -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__)

View File

@@ -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

View 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()

View File

@@ -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": {

View File

@@ -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()

View 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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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}", {})

View File

@@ -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):

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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__(

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -4,9 +4,4 @@ DOMAIN = "kmtronic"
CONF_REVERSE = "reverse"
DATA_HUB = "hub"
DATA_COORDINATOR = "coordinator"
MANUFACTURER = "KMtronic"
UPDATE_LISTENER = "update_listener"

View 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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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,

View File

@@ -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()

View File

@@ -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 = []

View File

@@ -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(

View File

@@ -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 = []

View File

@@ -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 = []

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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,
}

View File

@@ -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:

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(
(

View File

@@ -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)

View File

@@ -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

View File

@@ -57,6 +57,9 @@
"bat_replacement_description": {
"default": "mdi:battery-sync"
},
"battery_voltage": {
"default": "mdi:current-dc"
},
"flow": {
"default": "mdi:pipe"
},

View File

@@ -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,

View File

@@ -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