forked from home-assistant/core
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1929a0d6dd | |||
| 1adab9a982 | |||
| cd51070219 | |||
| 3c91c78383 | |||
| 96e0d1f5c6 | |||
| 2859e7de9b | |||
| 88683a318d | |||
| 84e9422254 | |||
| fde36d5034 | |||
| 8c1e43c07c | |||
| 05343392a7 | |||
| 32314dbb13 | |||
| 8f661fc5cf | |||
| e315cb9859 | |||
| d0e77eb1e2 | |||
| e23cac8bef | |||
| 973700542b | |||
| 2e21493c19 | |||
| 73bed96a0f | |||
| 0a5d13f104 | |||
| d16ec81727 | |||
| 11564e3df5 | |||
| 341d9f15f0 | |||
| 2c13c70e12 | |||
| 73d0d87705 | |||
| b8dfb2c850 | |||
| cf67a68454 | |||
| b003429912 | |||
| 4aff032442 | |||
| da3d8a6332 | |||
| 7a5c088149 | |||
| 31eec6f471 | |||
| c602a0e279 | |||
| 513045e489 | |||
| 0db6520802 | |||
| 5bc2e271d2 | |||
| 77dca49c75 | |||
| 1baba8b880 | |||
| 875d81cab2 |
@@ -1,15 +1,14 @@
|
||||
name: Report an issue with Home Assistant Core
|
||||
description: Report an issue with Home Assistant Core.
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for reporting bugs only!
|
||||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||
If you have a feature or enhancement request, please [request them here instead][fr].
|
||||
|
||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||
[fr]: https://github.com/orgs/home-assistant/discussions
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -10,8 +10,8 @@ contact_links:
|
||||
url: https://www.home-assistant.io/help
|
||||
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||
- name: Feature Request
|
||||
url: https://community.home-assistant.io/c/feature-requests
|
||||
about: Please use our Community Forum for making feature requests.
|
||||
url: https://github.com/orgs/home-assistant/discussions
|
||||
about: Please use this link to request new features or enhancements to existing features.
|
||||
- name: I'm unsure where to go
|
||||
url: https://www.home-assistant.io/join-chat
|
||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||
|
||||
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 2
|
||||
CACHE_VERSION: 3
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.7"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.12
|
||||
rev: v0.12.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
Generated
-2
@@ -784,8 +784,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/jellyfin/ @RunC0deRun @ctalkington
|
||||
/homeassistant/components/jewish_calendar/ @tsvi
|
||||
/tests/components/jewish_calendar/ @tsvi
|
||||
/homeassistant/components/juicenet/ @jesserockz
|
||||
/tests/components/juicenet/ @jesserockz
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
|
||||
@@ -38,8 +38,7 @@ def validate_python() -> None:
|
||||
|
||||
def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import config as config_util
|
||||
from . import config as config_util # noqa: PLC0415
|
||||
|
||||
lib_dir = os.path.join(config_dir, "deps")
|
||||
|
||||
@@ -80,8 +79,7 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import config as config_util
|
||||
from . import config as config_util # noqa: PLC0415
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Home Assistant: Observe, Control, Automate.",
|
||||
@@ -177,8 +175,7 @@ def main() -> int:
|
||||
validate_os()
|
||||
|
||||
if args.script is not None:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import scripts
|
||||
from . import scripts # noqa: PLC0415
|
||||
|
||||
return scripts.run(args.script)
|
||||
|
||||
@@ -188,8 +185,7 @@ def main() -> int:
|
||||
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import config, runner
|
||||
from . import config, runner # noqa: PLC0415
|
||||
|
||||
safe_mode = config.safe_mode_enabled(config_dir)
|
||||
|
||||
|
||||
@@ -52,28 +52,28 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def _generate_secret() -> str:
|
||||
"""Generate a secret."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return str(pyotp.random_base32())
|
||||
|
||||
|
||||
def _generate_random() -> int:
|
||||
"""Generate a 32 digit number."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
|
||||
|
||||
|
||||
def _generate_otp(secret: str, count: int) -> str:
|
||||
"""Generate one time password."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return str(pyotp.HOTP(secret).at(count))
|
||||
|
||||
|
||||
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
||||
"""Verify one time password."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return bool(pyotp.HOTP(secret).verify(otp, count))
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
import pyqrcode # pylint: disable=import-outside-toplevel
|
||||
import pyqrcode # noqa: PLC0415
|
||||
|
||||
qr_code = pyqrcode.create(data)
|
||||
|
||||
@@ -59,7 +59,7 @@ def _generate_qr_code(data: str) -> str:
|
||||
|
||||
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
||||
"""Generate a secret, url, and QR code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
ota_secret = pyotp.random_base32()
|
||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||
@@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
ota_secret: str = secret or pyotp.random_base32()
|
||||
|
||||
@@ -163,7 +163,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
||||
# even we cannot find user, we still do verify
|
||||
@@ -196,7 +196,7 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ async def async_setup_hass(
|
||||
|
||||
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||
"""Open the UI."""
|
||||
import webbrowser # pylint: disable=import-outside-toplevel
|
||||
import webbrowser # noqa: PLC0415
|
||||
|
||||
if hass.config.api is None or "frontend" not in hass.config.components:
|
||||
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
||||
@@ -561,8 +561,7 @@ async def async_enable_logging(
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from colorlog import ColoredFormatter
|
||||
from colorlog import ColoredFormatter # noqa: PLC0415
|
||||
|
||||
# basicConfig must be called after importing colorlog in order to
|
||||
# ensure that the handlers it sets up wraps the correct streams.
|
||||
@@ -606,7 +605,7 @@ async def async_enable_logging(
|
||||
)
|
||||
threading.excepthook = lambda args: logging.getLogger().exception(
|
||||
"Uncaught thread exception",
|
||||
exc_info=( # type: ignore[arg-type]
|
||||
exc_info=( # type: ignore[arg-type] # noqa: LOG014
|
||||
args.exc_type,
|
||||
args.exc_value,
|
||||
args.exc_traceback,
|
||||
@@ -1060,5 +1059,5 @@ async def _async_setup_multi_components(
|
||||
_LOGGER.error(
|
||||
"Error setting up integration %s - received exception",
|
||||
domain,
|
||||
exc_info=(type(result), result, result.__traceback__),
|
||||
exc_info=(type(result), result, result.__traceback__), # noqa: LOG014
|
||||
)
|
||||
|
||||
@@ -39,14 +39,14 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
try:
|
||||
location_point_valid = await test_location(
|
||||
location_point_valid = await check_location(
|
||||
websession,
|
||||
user_input["api_key"],
|
||||
user_input["latitude"],
|
||||
user_input["longitude"],
|
||||
)
|
||||
if not location_point_valid:
|
||||
location_nearest_valid = await test_location(
|
||||
location_nearest_valid = await check_location(
|
||||
websession,
|
||||
user_input["api_key"],
|
||||
user_input["latitude"],
|
||||
@@ -88,7 +88,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
async def test_location(
|
||||
async def check_location(
|
||||
client: ClientSession,
|
||||
api_key: str,
|
||||
latitude: float,
|
||||
|
||||
@@ -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_id,
|
||||
vol.Optional("question"): str,
|
||||
vol.Optional("question_media_id"): str,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("answers"): [
|
||||
{
|
||||
vol.Required("id"): str,
|
||||
vol.Required("sentences"): vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string],
|
||||
has_one_non_empty_item,
|
||||
has_no_punctuation,
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
cv.has_at_least_one_key("question", "question_media_id"),
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.data[CONNECTION_TEST_DATA] = {}
|
||||
async_register_websocket_api(hass)
|
||||
hass.http.register_view(ConnectionTestView())
|
||||
@@ -110,3 +178,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||
|
||||
|
||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
"""Validate result does not contain punctuation."""
|
||||
for sentence in value:
|
||||
if (
|
||||
PUNCTUATION_START.search(sentence)
|
||||
or PUNCTUATION_END.search(sentence)
|
||||
or PUNCTUATION_START_WORD.search(sentence)
|
||||
or PUNCTUATION_END_WORD.search(sentence)
|
||||
):
|
||||
raise vol.Invalid("sentence should not contain punctuation")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||
"""Validate result has at least one item."""
|
||||
if len(value) < 1:
|
||||
raise vol.Invalid("at least one sentence is required")
|
||||
|
||||
for sentence in value:
|
||||
if not sentence:
|
||||
raise vol.Invalid("sentences cannot be empty")
|
||||
|
||||
return value
|
||||
|
||||
@@ -4,12 +4,16 @@ from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Literal, final
|
||||
|
||||
from hassil import Intents, recognize
|
||||
from hassil.expression import Expression, ListReference, Sequence
|
||||
from hassil.intents import WildcardSlotList
|
||||
|
||||
from homeassistant.components import conversation, media_source, stt, tts
|
||||
from homeassistant.components.assist_pipeline import (
|
||||
OPTION_PREFERRED,
|
||||
@@ -105,6 +109,20 @@ class AssistSatelliteAnnouncement:
|
||||
"""Media ID to be played before announcement."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssistSatelliteAnswer:
|
||||
"""Answer to a question."""
|
||||
|
||||
id: str | None
|
||||
"""Matched answer id or None if no answer was matched."""
|
||||
|
||||
sentence: str
|
||||
"""Raw sentence text from user response."""
|
||||
|
||||
slots: dict[str, Any] = field(default_factory=dict)
|
||||
"""Matched slots from answer."""
|
||||
|
||||
|
||||
class AssistSatelliteEntity(entity.Entity):
|
||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||
|
||||
@@ -120,8 +138,10 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
_is_announcing = False
|
||||
_extra_system_prompt: str | None = None
|
||||
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
|
||||
_stt_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 +329,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 +477,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 +564,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 +584,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 +724,15 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
media_id_source=media_id_source,
|
||||
preannounce_media_id=preannounce_media_id,
|
||||
)
|
||||
|
||||
|
||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||
"""Collect list reference names recursively."""
|
||||
if isinstance(expression, Sequence):
|
||||
seq: Sequence = expression
|
||||
for item in seq.items:
|
||||
_collect_list_references(item, list_names)
|
||||
elif isinstance(expression, ListReference):
|
||||
# {list}
|
||||
list_ref: ListReference = expression
|
||||
list_names.add(list_ref.slot_name)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
},
|
||||
"start_conversation": {
|
||||
"service": "mdi:forum"
|
||||
},
|
||||
"ask_question": {
|
||||
"service": "mdi:microphone-question"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"dependencies": ["assist_pipeline", "http", "stt", "tts"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal"
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -54,3 +54,35 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
ask_question:
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
supported_features:
|
||||
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
|
||||
question:
|
||||
required: false
|
||||
example: "What kind of music would you like to play?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
question_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
answers:
|
||||
required: false
|
||||
selector:
|
||||
object:
|
||||
|
||||
@@ -59,6 +59,36 @@
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ask_question": {
|
||||
"name": "Ask question",
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Assist satellite entity to ask the question on."
|
||||
},
|
||||
"question": {
|
||||
"name": "Question",
|
||||
"description": "The question to ask."
|
||||
},
|
||||
"question_media_id": {
|
||||
"name": "Question media ID",
|
||||
"description": "The media ID of the question to use instead of text-to-speech."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the start message or media."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
},
|
||||
"answers": {
|
||||
"name": "Answers",
|
||||
"description": "Possible answers to the question."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ DATA_BLUEPRINTS = "automation_blueprints"
|
||||
|
||||
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
||||
"""Return True if any automation references the blueprint."""
|
||||
from . import automations_with_blueprint # pylint: disable=import-outside-toplevel
|
||||
from . import automations_with_blueprint # noqa: PLC0415
|
||||
|
||||
return len(automations_with_blueprint(hass, blueprint_path)) > 0
|
||||
|
||||
@@ -28,8 +28,7 @@ async def _reload_blueprint_automations(
|
||||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
||||
"""Get automation blueprints."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .config import AUTOMATION_BLUEPRINT_SCHEMA
|
||||
from .config import AUTOMATION_BLUEPRINT_SCHEMA # noqa: PLC0415
|
||||
|
||||
return blueprint.DomainBlueprints(
|
||||
hass,
|
||||
|
||||
@@ -94,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if not with_hassio:
|
||||
reader_writer = CoreBackupReaderWriter(hass)
|
||||
else:
|
||||
# pylint: disable-next=import-outside-toplevel, hass-component-root-import
|
||||
from homeassistant.components.hassio.backup import SupervisorBackupReaderWriter
|
||||
# pylint: disable-next=hass-component-root-import
|
||||
from homeassistant.components.hassio.backup import ( # noqa: PLC0415
|
||||
SupervisorBackupReaderWriter,
|
||||
)
|
||||
|
||||
reader_writer = SupervisorBackupReaderWriter(hass)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -156,6 +156,15 @@ class CameraWebRTCProvider(ABC):
|
||||
"""Close the session."""
|
||||
return ## This is an optional method so we need a default here.
|
||||
|
||||
async def async_get_image(
|
||||
self,
|
||||
camera: Camera,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> bytes | None:
|
||||
"""Get an image from the camera."""
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_webrtc_provider(
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.102.0"],
|
||||
"requirements": ["hass-nabucasa==0.103.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -54,10 +54,10 @@ class Control4RuntimeData:
|
||||
type Control4ConfigEntry = ConfigEntry[Control4RuntimeData]
|
||||
|
||||
|
||||
async def call_c4_api_retry(func, *func_args):
|
||||
async def call_c4_api_retry(func, *func_args): # noqa: RET503
|
||||
"""Call C4 API function and retry on failure."""
|
||||
# Ruff doesn't understand this loop - the exception is always raised after the retries
|
||||
for i in range(API_RETRY_TIMES): # noqa: RET503
|
||||
for i in range(API_RETRY_TIMES):
|
||||
try:
|
||||
return await func(*func_args)
|
||||
except client_exceptions.ClientError as exception:
|
||||
|
||||
@@ -271,7 +271,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
# Temporary migration. We can remove this in 2024.10
|
||||
from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel
|
||||
from homeassistant.components.assist_pipeline import ( # noqa: PLC0415
|
||||
async_migrate_engine,
|
||||
)
|
||||
|
||||
|
||||
@@ -108,8 +108,7 @@ def download_file(service: ServiceCall) -> None:
|
||||
_LOGGER.debug("%s -> %s", url, final_path)
|
||||
|
||||
with open(final_path, "wb") as fil:
|
||||
for chunk in req.iter_content(1024):
|
||||
fil.write(chunk)
|
||||
fil.writelines(req.iter_content(1024))
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
service.hass.bus.fire(
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent
|
||||
from deebot_client.events.base import Event
|
||||
from deebot_client.events.water_info import MopAttachedEvent
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -16,15 +16,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||
from .util import get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsBinarySensorEntityDescription(
|
||||
class EcovacsBinarySensorEntityDescription[EventT: Event](
|
||||
BinarySensorEntityDescription,
|
||||
EcovacsCapabilityEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Class describing Deebot binary sensor entity."""
|
||||
|
||||
@@ -55,7 +54,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class EcovacsBinarySensor(
|
||||
class EcovacsBinarySensor[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilityEvent[EventT]],
|
||||
BinarySensorEntity,
|
||||
):
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any
|
||||
|
||||
from deebot_client.capabilities import Capabilities
|
||||
from deebot_client.device import Device
|
||||
@@ -18,11 +18,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CapabilityEntity = TypeVar("CapabilityEntity")
|
||||
EventT = TypeVar("EventT", bound=Event)
|
||||
|
||||
|
||||
class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
class EcovacsEntity[CapabilityEntityT](Entity):
|
||||
"""Ecovacs entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
@@ -32,7 +29,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilityEntity,
|
||||
capability: CapabilityEntityT,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
@@ -80,7 +77,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
|
||||
self._subscribe(AvailabilityEvent, on_available)
|
||||
|
||||
def _subscribe(
|
||||
def _subscribe[EventT: Event](
|
||||
self,
|
||||
event_type: type[EventT],
|
||||
callback: Callable[[EventT], Coroutine[Any, Any, None]],
|
||||
@@ -98,13 +95,13 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
||||
self._device.events.request_refresh(event_type)
|
||||
|
||||
|
||||
class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
|
||||
class EcovacsDescriptionEntity[CapabilityEntityT](EcovacsEntity[CapabilityEntityT]):
|
||||
"""Ecovacs entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilityEntity,
|
||||
capability: CapabilityEntityT,
|
||||
entity_description: EntityDescription,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
@@ -114,13 +111,12 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsCapabilityEntityDescription(
|
||||
class EcovacsCapabilityEntityDescription[CapabilityEntityT](
|
||||
EntityDescription,
|
||||
Generic[CapabilityEntity],
|
||||
):
|
||||
"""Ecovacs entity description."""
|
||||
|
||||
capability_fn: Callable[[Capabilities], CapabilityEntity | None]
|
||||
capability_fn: Callable[[Capabilities], CapabilityEntityT | None]
|
||||
|
||||
|
||||
class EcovacsLegacyEntity(Entity):
|
||||
|
||||
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilitySet
|
||||
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
|
||||
from deebot_client.events.base import Event
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
@@ -23,16 +23,14 @@ from .entity import (
|
||||
EcovacsCapabilityEntityDescription,
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsNumberEntityDescription(
|
||||
class EcovacsNumberEntityDescription[EventT: Event](
|
||||
NumberEntityDescription,
|
||||
EcovacsCapabilityEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Ecovacs number entity description."""
|
||||
|
||||
@@ -94,7 +92,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EcovacsNumberEntity(
|
||||
class EcovacsNumberEntity[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]],
|
||||
NumberEntity,
|
||||
):
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic
|
||||
from typing import Any
|
||||
|
||||
from deebot_client.capabilities import CapabilitySetTypes
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.events import WorkModeEvent
|
||||
from deebot_client.events.base import Event
|
||||
from deebot_client.events.water_info import WaterAmountEvent
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
@@ -15,15 +16,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||
from .util import get_name_key, get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsSelectEntityDescription(
|
||||
class EcovacsSelectEntityDescription[EventT: Event](
|
||||
SelectEntityDescription,
|
||||
EcovacsCapabilityEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Ecovacs select entity description."""
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EcovacsSelectEntity(
|
||||
class EcovacsSelectEntity[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]],
|
||||
SelectEntity,
|
||||
):
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic
|
||||
from typing import Any
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
||||
from deebot_client.device import Device
|
||||
@@ -46,16 +46,14 @@ from .entity import (
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsEntity,
|
||||
EcovacsLegacyEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_name_key, get_options, get_supported_entities
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsSensorEntityDescription(
|
||||
class EcovacsSensorEntityDescription[EventT: Event](
|
||||
EcovacsCapabilityEntityDescription,
|
||||
SensorEntityDescription,
|
||||
Generic[EventT],
|
||||
):
|
||||
"""Ecovacs sensor entity description."""
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -30,16 +30,16 @@ from .entity import EheimDigitalEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalNumberDescription(NumberEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
|
||||
NumberEntityDescription
|
||||
):
|
||||
"""Class describing EHEIM Digital sensor entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], float | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, float], Awaitable[None]]
|
||||
uom_fn: Callable[[_DeviceT_co], str] | None = None
|
||||
value_fn: Callable[[_DeviceT], float | None]
|
||||
set_value_fn: Callable[[_DeviceT, float], Awaitable[None]]
|
||||
uom_fn: Callable[[_DeviceT], str] | None = None
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -136,7 +136,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the number entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalNumber[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalNumber[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
@@ -163,18 +163,18 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalNumber(
|
||||
EheimDigitalEntity[_DeviceT_co], NumberEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalNumber[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], NumberEntity
|
||||
):
|
||||
"""Represent a EHEIM Digital number entity."""
|
||||
|
||||
entity_description: EheimDigitalNumberDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalNumberDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalNumberDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalNumberDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital number entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -17,15 +17,15 @@ from .entity import EheimDigitalEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
|
||||
SelectEntityDescription
|
||||
):
|
||||
"""Class describing EHEIM Digital select entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], str | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
|
||||
value_fn: Callable[[_DeviceT], str | None]
|
||||
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -59,7 +59,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the number entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalSelect[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
@@ -75,18 +75,18 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalSelect(
|
||||
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], SelectEntity
|
||||
):
|
||||
"""Represent an EHEIM Digital select entity."""
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalSelectDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalSelectDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital select entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -20,14 +20,14 @@ from .entity import EheimDigitalEntity
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice](
|
||||
SensorEntityDescription
|
||||
):
|
||||
"""Class describing EHEIM Digital sensor entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], float | str | None]
|
||||
value_fn: Callable[[_DeviceT], float | str | None]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the light entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalSensor[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities += [
|
||||
@@ -91,18 +91,18 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalSensor(
|
||||
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalSensor[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], SensorEntity
|
||||
):
|
||||
"""Represent a EHEIM Digital sensor entity."""
|
||||
|
||||
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalSensorDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalSensorDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalSensorDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital number entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import time
|
||||
from typing import Generic, TypeVar, final, override
|
||||
from typing import Any, final, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -19,15 +19,13 @@ from .entity import EheimDigitalEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalTimeDescription(TimeEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescription):
|
||||
"""Class describing EHEIM Digital time entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], time | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, time], Awaitable[None]]
|
||||
value_fn: Callable[[_DeviceT], time | None]
|
||||
set_value_fn: Callable[[_DeviceT, time], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -79,7 +77,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the time entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalTime[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalTime[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
@@ -103,18 +101,18 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
@final
|
||||
class EheimDigitalTime(
|
||||
EheimDigitalEntity[_DeviceT_co], TimeEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalTime[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], TimeEntity
|
||||
):
|
||||
"""Represent an EHEIM Digital time entity."""
|
||||
|
||||
entity_description: EheimDigitalTimeDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalTimeDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalTimeDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalTimeDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital time entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -63,9 +63,7 @@ class ESPHomeDashboardManager:
|
||||
if not (data := self._data) or not (info := data.get("info")):
|
||||
return
|
||||
if is_hassio(self._hass):
|
||||
from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel
|
||||
get_addons_info,
|
||||
)
|
||||
from homeassistant.components.hassio import get_addons_info # noqa: PLC0415
|
||||
|
||||
if (addons := get_addons_info(self._hass)) is not None and info[
|
||||
"addon_slug"
|
||||
|
||||
@@ -364,8 +364,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
|
||||
if dev_repo_path is not None:
|
||||
return pathlib.Path(dev_repo_path) / "hass_frontend"
|
||||
# Keep import here so that we can import frontend without installing reqs
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import hass_frontend
|
||||
import hass_frontend # noqa: PLC0415
|
||||
|
||||
return hass_frontend.where()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -212,8 +212,7 @@ class AbstractConfig(ABC):
|
||||
def async_enable_report_state(self) -> None:
|
||||
"""Enable proactive mode."""
|
||||
# Circular dep
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .report_state import async_enable_report_state
|
||||
from .report_state import async_enable_report_state # noqa: PLC0415
|
||||
|
||||
if self._unsub_report_state is None:
|
||||
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
||||
@@ -395,8 +394,7 @@ class AbstractConfig(ABC):
|
||||
async def _handle_local_webhook(self, hass, webhook_id, request):
|
||||
"""Handle an incoming local SDK message."""
|
||||
# Circular dep
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import smart_home
|
||||
from . import smart_home # noqa: PLC0415
|
||||
|
||||
self._local_last_active = utcnow()
|
||||
|
||||
@@ -655,8 +653,9 @@ class GoogleEntity:
|
||||
if "matter" in self.hass.config.components and any(
|
||||
x for x in device_entry.identifiers if x[0] == "matter"
|
||||
):
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.matter import get_matter_device_info
|
||||
from homeassistant.components.matter import ( # noqa: PLC0415
|
||||
get_matter_device_info,
|
||||
)
|
||||
|
||||
# Import matter can block the event loop for multiple seconds
|
||||
# so we import it here to avoid blocking the event loop during
|
||||
|
||||
@@ -29,8 +29,7 @@ async def update_addon(
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
if backup:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .backup import backup_addon_before_update
|
||||
from .backup import backup_addon_before_update # noqa: PLC0415
|
||||
|
||||
await backup_addon_before_update(hass, addon, addon_name, installed_version)
|
||||
|
||||
@@ -50,8 +49,7 @@ async def update_core(hass: HomeAssistant, version: str | None, backup: bool) ->
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
if backup:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .backup import backup_core_before_update
|
||||
from .backup import backup_core_before_update # noqa: PLC0415
|
||||
|
||||
await backup_core_before_update(hass)
|
||||
|
||||
@@ -71,8 +69,7 @@ async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> N
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
if backup:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .backup import backup_core_before_update
|
||||
from .backup import backup_core_before_update # noqa: PLC0415
|
||||
|
||||
await backup_core_before_update(hass)
|
||||
|
||||
|
||||
@@ -309,8 +309,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Set up the options flow."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.radio_manager import (
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
ZhaMultiPANMigrationHelper,
|
||||
)
|
||||
|
||||
@@ -451,16 +450,11 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Configure the Silicon Labs Multiprotocol add-on."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.radio_manager import (
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
ZhaMultiPANMigrationHelper,
|
||||
)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.silabs_multiprotocol import (
|
||||
from homeassistant.components.zha.silabs_multiprotocol import ( # noqa: PLC0415
|
||||
async_get_channel as async_get_zha_channel,
|
||||
)
|
||||
|
||||
@@ -747,11 +741,8 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform initial backup and reconfigure ZHA."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.radio_manager import (
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
ZhaMultiPANMigrationHelper,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
|
||||
entry.runtime_data = homee
|
||||
entry.async_on_unload(homee.disconnect)
|
||||
|
||||
def _connection_update_callback(connected: bool) -> None:
|
||||
async def _connection_update_callback(connected: bool) -> None:
|
||||
"""Call when the device is notified of changes."""
|
||||
if connected:
|
||||
_LOGGER.warning("Reconnected to Homee at %s", entry.data[CONF_HOST])
|
||||
|
||||
@@ -28,6 +28,7 @@ class HomeeEntity(Entity):
|
||||
self._entry = entry
|
||||
node = entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||
# Homee hub itself has node-id -1
|
||||
assert node is not None
|
||||
if node.id == -1:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
|
||||
@@ -79,7 +80,7 @@ class HomeeEntity(Entity):
|
||||
def _on_node_updated(self, attribute: HomeeAttribute) -> None:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _on_connection_changed(self, connected: bool) -> None:
|
||||
async def _on_connection_changed(self, connected: bool) -> None:
|
||||
self._host_connected = connected
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -166,6 +167,6 @@ class HomeeNodeEntity(Entity):
|
||||
def _on_node_updated(self, node: HomeeNode) -> None:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _on_connection_changed(self, connected: bool) -> None:
|
||||
async def _on_connection_changed(self, connected: bool) -> None:
|
||||
self._host_connected = connected
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -58,9 +58,13 @@ class HomeeLock(HomeeEntity, LockEntity):
|
||||
AttributeChangedBy, self._attribute.changed_by
|
||||
)
|
||||
if self._attribute.changed_by == AttributeChangedBy.USER:
|
||||
changed_id = self._entry.runtime_data.get_user_by_id(
|
||||
user = self._entry.runtime_data.get_user_by_id(
|
||||
self._attribute.changed_by_id
|
||||
).username
|
||||
)
|
||||
if user is not None:
|
||||
changed_id = user.username
|
||||
else:
|
||||
changed_id = "Unknown"
|
||||
|
||||
return f"{changed_by_name}-{changed_id}"
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["homee"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyHomee==1.2.8"]
|
||||
"requirements": ["pyHomee==1.2.9"]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ def get_device_class(
|
||||
) -> SwitchDeviceClass:
|
||||
"""Check device class of Switch according to node profile."""
|
||||
node = config_entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||
assert node is not None
|
||||
if node.profile in [
|
||||
NodeProfile.ON_OFF_PLUG,
|
||||
NodeProfile.METERING_PLUG,
|
||||
|
||||
@@ -355,11 +355,10 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="ignored_model")
|
||||
|
||||
# Late imports in case BLE is not available
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from aiohomekit.controller.ble.discovery import BleDiscovery
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from aiohomekit.controller.ble.manufacturer_data import HomeKitAdvertisement
|
||||
from aiohomekit.controller.ble.discovery import BleDiscovery # noqa: PLC0415
|
||||
from aiohomekit.controller.ble.manufacturer_data import ( # noqa: PLC0415
|
||||
HomeKitAdvertisement,
|
||||
)
|
||||
|
||||
mfr_data = discovery_info.manufacturer_data
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.5"]
|
||||
"requirements": ["homematicip==2.0.6"]
|
||||
}
|
||||
|
||||
@@ -278,8 +278,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
ssl_certificate is not None
|
||||
and (hass.config.external_url or hass.config.internal_url) is None
|
||||
):
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.cloud import (
|
||||
from homeassistant.components.cloud import ( # noqa: PLC0415
|
||||
CloudNotAvailable,
|
||||
async_remote_ui_url,
|
||||
)
|
||||
@@ -511,12 +510,14 @@ class HomeAssistantHTTP:
|
||||
) -> None:
|
||||
"""Register a folder or file to serve as a static path."""
|
||||
frame.report_usage(
|
||||
"calls hass.http.register_static_path which is deprecated because "
|
||||
"it does blocking I/O in the event loop, instead "
|
||||
"calls hass.http.register_static_path which "
|
||||
"does blocking I/O in the event loop, instead "
|
||||
"call `await hass.http.async_register_static_paths("
|
||||
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`',
|
||||
exclude_integrations={"http"},
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
core_behavior=frame.ReportBehavior.ERROR,
|
||||
core_integration_behavior=frame.ReportBehavior.ERROR,
|
||||
custom_integration_behavior=frame.ReportBehavior.ERROR,
|
||||
breaks_in_ha_version="2025.7",
|
||||
)
|
||||
configs = [StaticPathConfig(url_path, path, cache_headers)]
|
||||
|
||||
@@ -136,8 +136,7 @@ async def process_wrong_login(request: Request) -> None:
|
||||
_LOGGER.warning(log_msg)
|
||||
|
||||
# Circular import with websocket_api
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components import persistent_notification # noqa: PLC0415
|
||||
|
||||
persistent_notification.async_create(
|
||||
hass, notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN
|
||||
|
||||
@@ -444,8 +444,9 @@ class TimerManager:
|
||||
timer.finish()
|
||||
|
||||
if timer.conversation_command:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.conversation import async_converse
|
||||
from homeassistant.components.conversation import ( # noqa: PLC0415
|
||||
async_converse,
|
||||
)
|
||||
|
||||
self.hass.async_create_background_task(
|
||||
async_converse(
|
||||
|
||||
@@ -1,109 +1,36 @@
|
||||
"""The JuiceNet integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from pyjuicenet import Api, TokenError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .device import JuiceNetApi
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
cv.deprecated(DOMAIN),
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the JuiceNet component."""
|
||||
conf = config.get(DOMAIN)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if not conf:
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||
)
|
||||
)
|
||||
return True
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up JuiceNet from a config entry."""
|
||||
|
||||
config = entry.data
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
access_token = config[CONF_ACCESS_TOKEN]
|
||||
api = Api(access_token, session)
|
||||
|
||||
juicenet = JuiceNetApi(api)
|
||||
|
||||
try:
|
||||
await juicenet.setup()
|
||||
except TokenError as error:
|
||||
_LOGGER.error("JuiceNet Error %s", error)
|
||||
return False
|
||||
except aiohttp.ClientError as error:
|
||||
_LOGGER.error("Could not reach the JuiceNet API %s", error)
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
if not juicenet.devices:
|
||||
_LOGGER.error("No JuiceNet devices found for this account")
|
||||
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(
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="JuiceNet",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=30),
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/juicenet",
|
||||
},
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
JUICENET_API: juicenet,
|
||||
JUICENET_COORDINATOR: coordinator,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,82 +1,11 @@
|
||||
"""Config flow for JuiceNet integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from pyjuicenet import Api, TokenError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core, exceptions
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
juicenet = Api(data[CONF_ACCESS_TOKEN], session)
|
||||
|
||||
try:
|
||||
await juicenet.get_devices()
|
||||
except TokenError as error:
|
||||
_LOGGER.error("Token Error %s", error)
|
||||
raise InvalidAuth from error
|
||||
except aiohttp.ClientError as error:
|
||||
_LOGGER.error("Error connecting %s", error)
|
||||
raise CannotConnect from error
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
return {"title": "JuiceNet"}
|
||||
|
||||
|
||||
class JuiceNetConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for JuiceNet."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import."""
|
||||
return await self.async_step_user(import_data)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
"""Constants used by the JuiceNet component."""
|
||||
|
||||
DOMAIN = "juicenet"
|
||||
|
||||
JUICENET_API = "juicenet_api"
|
||||
JUICENET_COORDINATOR = "juicenet_coordinator"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
||||
|
||||
|
||||
class JuiceNetApi:
|
||||
"""Represent a connection to JuiceNet."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Create an object from the provided API instance."""
|
||||
self.api = api
|
||||
self._devices = []
|
||||
|
||||
async def setup(self):
|
||||
"""JuiceNet device setup."""
|
||||
self._devices = await self.api.get_devices()
|
||||
|
||||
@property
|
||||
def devices(self) -> list:
|
||||
"""Get a list of devices managed by this account."""
|
||||
return self._devices
|
||||
@@ -1,34 +0,0 @@
|
||||
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
||||
|
||||
from pyjuicenet import Charger
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class JuiceNetDevice(CoordinatorEntity):
|
||||
"""Represent a base JuiceNet device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialise the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.device = device
|
||||
self.key = key
|
||||
self._attr_unique_id = f"{device.id}-{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=(
|
||||
f"https://home.juice.net/Portal/Details?unitID={device.id}"
|
||||
),
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
manufacturer="JuiceNet",
|
||||
name=device.name,
|
||||
)
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"domain": "juicenet",
|
||||
"name": "JuiceNet",
|
||||
"codeowners": ["@jesserockz"],
|
||||
"config_flow": true,
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/juicenet",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyjuicenet"],
|
||||
"requirements": ["python-juicenet==1.1.0"]
|
||||
"requirements": []
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyjuicenet import Api, Charger
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DEFAULT_MAX_VALUE,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class JuiceNetNumberEntityDescription(NumberEntityDescription):
|
||||
"""An entity description for a JuiceNetNumber."""
|
||||
|
||||
setter_key: str
|
||||
native_max_value_key: str | None = None
|
||||
|
||||
|
||||
NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = (
|
||||
JuiceNetNumberEntityDescription(
|
||||
translation_key="amperage_limit",
|
||||
key="current_charging_amperage_limit",
|
||||
native_min_value=6,
|
||||
native_max_value_key="max_charging_amperage",
|
||||
native_step=1,
|
||||
setter_key="set_charging_amperage_limit",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> 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]
|
||||
|
||||
entities = [
|
||||
JuiceNetNumber(device, description, coordinator)
|
||||
for device in api.devices
|
||||
for description in NUMBER_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetNumber(JuiceNetDevice, NumberEntity):
|
||||
"""Implementation of a JuiceNet number."""
|
||||
|
||||
entity_description: JuiceNetNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Charger,
|
||||
description: JuiceNetNumberEntityDescription,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialise the number."""
|
||||
super().__init__(device, description.key, coordinator)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the value of the entity."""
|
||||
return getattr(self.device, self.entity_description.key, None)
|
||||
|
||||
@property
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
if self.entity_description.native_max_value_key is not None:
|
||||
return getattr(self.device, self.entity_description.native_max_value_key)
|
||||
if self.entity_description.native_max_value is not None:
|
||||
return self.entity_description.native_max_value
|
||||
return DEFAULT_MAX_VALUE
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
await getattr(self.device, self.entity_description.setter_key)(value)
|
||||
@@ -1,117 +0,0 @@
|
||||
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="status",
|
||||
name="Charging Status",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="amps",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="watts",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="charge_time",
|
||||
translation_key="charge_time",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_added",
|
||||
translation_key="energy_added",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> 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]
|
||||
|
||||
entities = [
|
||||
JuiceNetSensorDevice(device, coordinator, description)
|
||||
for device in api.devices
|
||||
for description in SENSOR_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
|
||||
"""Implementation of a JuiceNet sensor."""
|
||||
|
||||
def __init__(
|
||||
self, device, coordinator, description: SensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialise the sensor."""
|
||||
super().__init__(device, description.key, coordinator)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
icon = None
|
||||
if self.entity_description.key == "status":
|
||||
status = self.device.status
|
||||
if status == "standby":
|
||||
icon = "mdi:power-plug-off"
|
||||
elif status == "plugged":
|
||||
icon = "mdi:power-plug"
|
||||
elif status == "charging":
|
||||
icon = "mdi:battery-positive"
|
||||
else:
|
||||
icon = self.entity_description.icon
|
||||
return icon
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state."""
|
||||
return getattr(self.device, self.entity_description.key, None)
|
||||
@@ -1,41 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"description": "You will need the API Token from https://home.juice.net/Manage.",
|
||||
"title": "Connect to JuiceNet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"amperage_limit": {
|
||||
"name": "Amperage limit"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"charge_time": {
|
||||
"name": "Charge time"
|
||||
},
|
||||
"energy_added": {
|
||||
"name": "Energy added"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"charge_now": {
|
||||
"name": "Charge now"
|
||||
}
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"title": "The JuiceNet integration has been removed",
|
||||
"description": "Enel X has dropped support for JuiceNet in favor of JuicePass, and the JuiceNet integration has been removed from Home Assistant as it was no longer working.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing JuiceNet integration entries]({entries})."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> 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]
|
||||
|
||||
async_add_entities(
|
||||
JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices
|
||||
)
|
||||
|
||||
|
||||
class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity):
|
||||
"""Implementation of a JuiceNet switch."""
|
||||
|
||||
_attr_translation_key = "charge_now"
|
||||
|
||||
def __init__(self, device, coordinator):
|
||||
"""Initialise the switch."""
|
||||
super().__init__(device, "charge_now", coordinator)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self.device.override_time != 0
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Charge now."""
|
||||
await self.device.set_override(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Don't charge now."""
|
||||
await self.device.set_override(False)
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import JustNimbusCoordinator
|
||||
from .const import PLATFORMS
|
||||
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||
"""Set up JustNimbus from a config entry."""
|
||||
if "zip_code" in entry.data:
|
||||
coordinator = JustNimbusCoordinator(hass, entry)
|
||||
@@ -18,13 +17,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryAuthFailed
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -16,13 +16,17 @@ from .const import CONF_ZIP_CODE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type JustNimbusConfigEntry = ConfigEntry[JustNimbusCoordinator]
|
||||
|
||||
|
||||
class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]):
|
||||
"""Data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: JustNimbusConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: JustNimbusConfigEntry
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
EntityCategory,
|
||||
@@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import JustNimbusCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||
from .entity import JustNimbusEntity
|
||||
|
||||
|
||||
@@ -102,16 +100,15 @@ SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: JustNimbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the JustNimbus sensor."""
|
||||
coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
JustNimbusSensor(
|
||||
device_id=entry.data[CONF_CLIENT_ID],
|
||||
description=description,
|
||||
coordinator=coordinator,
|
||||
coordinator=entry.runtime_data,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ from .const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
ROUTER,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from .router import KeeneticRouter
|
||||
|
||||
@@ -36,11 +35,10 @@ 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,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
@@ -50,8 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> 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
|
||||
)
|
||||
|
||||
@@ -6,7 +6,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()
|
||||
|
||||
@@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type KegtronConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool:
|
||||
"""Set up Kegtron BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = KegtronBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
@@ -42,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,11 +8,9 @@ from kegtron_ble import (
|
||||
Units,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -30,7 +28,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import KegtronConfigEntry
|
||||
from .device import device_key_to_bluetooth_entity_key
|
||||
|
||||
SENSOR_DESCRIPTIONS = {
|
||||
@@ -109,13 +107,11 @@ def sensor_update_to_bluetooth_data_update(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: KegtronConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Kegtron BLE sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
|
||||
@@ -2,26 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from microbot import MicroBotApiClient
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MicroBotDataUpdateCoordinator
|
||||
from .coordinator import MicroBotConfigEntry, MicroBotDataUpdateCoordinator
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
token: str = entry.data[CONF_ACCESS_TOKEN]
|
||||
bdaddr: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr)
|
||||
@@ -35,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, client=client, ble_device=ble_device
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
@@ -43,9 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -11,14 +11,15 @@ from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||
|
||||
type MicroBotConfigEntry = ConfigEntry[MicroBotDataUpdateCoordinator]
|
||||
|
||||
|
||||
class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
@@ -31,7 +32,7 @@ class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
ble_device: BLEDevice,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.api: MicroBotApiClient = client
|
||||
self.api = client
|
||||
self.data: dict[str, Any] = {}
|
||||
self.ble_device = ble_device
|
||||
super().__init__(
|
||||
|
||||
@@ -19,7 +19,7 @@ class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordin
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator, config_entry):
|
||||
def __init__(self, coordinator: MicroBotDataUpdateCoordinator) -> None:
|
||||
"""Initialise the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._address = self.coordinator.ble_device.address
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -16,8 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MicroBotDataUpdateCoordinator
|
||||
from .coordinator import MicroBotConfigEntry
|
||||
from .entity import MicroBotEntity
|
||||
|
||||
CALIBRATE = "calibrate"
|
||||
@@ -30,12 +28,11 @@ CALIBRATE_SCHEMA: VolDictType = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MicroBot based on a config entry."""
|
||||
coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([MicroBotBinarySwitch(coordinator, entry)])
|
||||
async_add_entities([MicroBotBinarySwitch(entry.runtime_data)])
|
||||
platform = async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
CALIBRATE,
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
"""The kmtronic integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from pykmtronic.auth import Auth
|
||||
from pykmtronic.hub import KMTronicHubAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER
|
||||
from .coordinator import KMTronicConfigEntry, KMtronicCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SWITCH]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool:
|
||||
"""Set up kmtronic from a config entry."""
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
auth = Auth(
|
||||
@@ -31,51 +22,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
hub = KMTronicHubAPI(auth)
|
||||
|
||||
async def async_update_data():
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await hub.async_update_relays()
|
||||
except aiohttp.client_exceptions.ClientResponseError as err:
|
||||
raise UpdateFailed(f"Wrong credentials: {err}") from err
|
||||
except aiohttp.client_exceptions.ClientConnectorError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{MANUFACTURER} {hub.name}",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
coordinator = KMtronicCoordinator(hass, entry, hub)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_HUB: hub,
|
||||
DATA_COORDINATOR: coordinator,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
update_listener = entry.add_update_listener(async_update_options)
|
||||
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
async def async_update_options(
|
||||
hass: HomeAssistant, config_entry: KMTronicConfigEntry
|
||||
) -> None:
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
|
||||
update_listener()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -4,9 +4,4 @@ DOMAIN = "kmtronic"
|
||||
|
||||
CONF_REVERSE = "reverse"
|
||||
|
||||
DATA_HUB = "hub"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
|
||||
MANUFACTURER = "KMtronic"
|
||||
|
||||
UPDATE_LISTENER = "update_listener"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""The kmtronic integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError
|
||||
from pykmtronic.hub import KMTronicHubAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import MANUFACTURER
|
||||
|
||||
PLATFORMS = [Platform.SWITCH]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type KMTronicConfigEntry = ConfigEntry[KMtronicCoordinator]
|
||||
|
||||
|
||||
class KMtronicCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Coordinator for KMTronic."""
|
||||
|
||||
entry: KMTronicConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: KMTronicConfigEntry, hub: KMTronicHubAPI
|
||||
) -> None:
|
||||
"""Initialize the KMTronic coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{MANUFACTURER} {hub.name}",
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
self.hub = hub
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch the latest data from the source."""
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await self.hub.async_update_relays()
|
||||
except ClientResponseError as err:
|
||||
raise UpdateFailed(f"Wrong credentials: {err}") from err
|
||||
except ClientConnectorError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
@@ -4,23 +4,23 @@ from typing import Any
|
||||
import urllib.parse
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER
|
||||
from .const import CONF_REVERSE, DOMAIN, MANUFACTURER
|
||||
from .coordinator import KMTronicConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: KMTronicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Config entry example."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
|
||||
hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB]
|
||||
coordinator = entry.runtime_data
|
||||
hub = coordinator.hub
|
||||
reverse = entry.options.get(CONF_REVERSE, False)
|
||||
await hub.async_get_relays()
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""The kodi component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection
|
||||
from pykodi.kodi import KodiHTTPConnection, KodiWSConnection
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -17,19 +19,23 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_WS_PORT,
|
||||
DATA_CONNECTION,
|
||||
DATA_KODI,
|
||||
DATA_REMOVE_LISTENER,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_WS_PORT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
type KodiConfigEntry = ConfigEntry[KodiRuntimeData]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@dataclass
|
||||
class KodiRuntimeData:
|
||||
"""Data class to hold Kodi runtime data."""
|
||||
|
||||
connection: KodiHTTPConnection | KodiWSConnection
|
||||
kodi: Kodi
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: KodiConfigEntry) -> bool:
|
||||
"""Set up Kodi from a config entry."""
|
||||
conn = get_kodi_connection(
|
||||
entry.data[CONF_HOST],
|
||||
@@ -58,26 +64,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def _close(event):
|
||||
await conn.close()
|
||||
|
||||
remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)
|
||||
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_CONNECTION: conn,
|
||||
DATA_KODI: kodi,
|
||||
DATA_REMOVE_LISTENER: remove_stop_listener,
|
||||
}
|
||||
entry.runtime_data = KodiRuntimeData(connection=conn, kodi=kodi)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: KodiConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
data = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await data[DATA_CONNECTION].close()
|
||||
data[DATA_REMOVE_LISTENER]()
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await entry.runtime_data.connection.close()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -4,10 +4,6 @@ DOMAIN = "kodi"
|
||||
|
||||
CONF_WS_PORT = "ws_port"
|
||||
|
||||
DATA_CONNECTION = "connection"
|
||||
DATA_KODI = "kodi"
|
||||
DATA_REMOVE_LISTENER = "remove_listener"
|
||||
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -55,6 +55,7 @@ from homeassistant.helpers.network import is_internal_request
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import KodiConfigEntry
|
||||
from .browse_media import (
|
||||
build_item_response,
|
||||
get_media_info,
|
||||
@@ -63,8 +64,6 @@ from .browse_media import (
|
||||
)
|
||||
from .const import (
|
||||
CONF_WS_PORT,
|
||||
DATA_CONNECTION,
|
||||
DATA_KODI,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_TIMEOUT,
|
||||
@@ -208,7 +207,7 @@ async def async_setup_platform(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: KodiConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Kodi media player platform."""
|
||||
@@ -220,14 +219,12 @@ async def async_setup_entry(
|
||||
SERVICE_CALL_METHOD, KODI_CALL_METHOD_SCHEMA, "async_call_method"
|
||||
)
|
||||
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
connection = data[DATA_CONNECTION]
|
||||
kodi = data[DATA_KODI]
|
||||
data = config_entry.runtime_data
|
||||
name = config_entry.data[CONF_NAME]
|
||||
if (uid := config_entry.unique_id) is None:
|
||||
uid = config_entry.entry_id
|
||||
|
||||
entity = KodiEntity(connection, kodi, name, uid)
|
||||
entity = KodiEntity(data.connection, data.kodi, name, uid)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ from .const import (
|
||||
PIN_TO_ZONE,
|
||||
STATE_HIGH,
|
||||
STATE_LOW,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
UPDATE_ENDPOINT,
|
||||
ZONE_TO_PIN,
|
||||
ZONES,
|
||||
@@ -261,10 +260,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# config entry specific data to enable unload
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: entry.add_update_listener(async_entry_updated)
|
||||
}
|
||||
entry.async_on_unload(entry.add_update_listener(async_entry_updated))
|
||||
return True
|
||||
|
||||
|
||||
@@ -272,11 +268,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][CONF_DEVICES].pop(entry.data[CONF_ID])
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -44,5 +44,3 @@ ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()}
|
||||
ENDPOINT_ROOT = "/api/konnected"
|
||||
UPDATE_ENDPOINT = ENDPOINT_ROOT + r"/device/{device_id:[a-zA-Z0-9]+}"
|
||||
SIGNAL_DS18B20_NEW = "konnected.ds18b20.new"
|
||||
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
@@ -4,42 +4,35 @@ import logging
|
||||
|
||||
from pykoplenti import ApiException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Plenticore
|
||||
from .coordinator import Plenticore, PlenticoreConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PlenticoreConfigEntry) -> bool:
|
||||
"""Set up Kostal Plenticore Solar Inverter from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
plenticore = Plenticore(hass, entry)
|
||||
|
||||
if not await plenticore.async_setup():
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = plenticore
|
||||
entry.runtime_data = plenticore
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PlenticoreConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
# remove API object
|
||||
plenticore = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
try:
|
||||
await plenticore.async_unload()
|
||||
await entry.runtime_data.async_unload()
|
||||
except ApiException as err:
|
||||
_LOGGER.error("Error logging out from inverter: %s", err)
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ from .helper import get_hostname_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type PlenticoreConfigEntry = ConfigEntry[Plenticore]
|
||||
|
||||
|
||||
class Plenticore:
|
||||
"""Manages the Plenticore API."""
|
||||
@@ -166,12 +168,12 @@ class DataUpdateCoordinatorMixin:
|
||||
class PlenticoreUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Base implementation of DataUpdateCoordinator for Plenticore data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PlenticoreConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PlenticoreConfigEntry,
|
||||
logger: logging.Logger,
|
||||
name: str,
|
||||
update_inverval: timedelta,
|
||||
@@ -248,12 +250,12 @@ class SettingDataUpdateCoordinator(
|
||||
class PlenticoreSelectUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Base implementation of DataUpdateCoordinator for Plenticore data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PlenticoreConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PlenticoreConfigEntry,
|
||||
logger: logging.Logger,
|
||||
name: str,
|
||||
update_inverval: timedelta,
|
||||
|
||||
@@ -5,23 +5,21 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED, async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_IDENTIFIERS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Plenticore
|
||||
from .coordinator import PlenticoreConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: PlenticoreConfigEntry
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)}
|
||||
|
||||
plenticore: Plenticore = hass.data[DOMAIN][config_entry.entry_id]
|
||||
plenticore = config_entry.runtime_data
|
||||
|
||||
# Get information from Kostal Plenticore library
|
||||
available_process_data = await plenticore.client.get_process_data()
|
||||
|
||||
@@ -14,15 +14,13 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SettingDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator
|
||||
from .helper import PlenticoreDataFormatter
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -74,11 +72,11 @@ NUMBER_SETTINGS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Kostal Plenticore Number entities."""
|
||||
plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
|
||||
@@ -7,15 +7,13 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Plenticore, SelectDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, SelectDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,11 +41,11 @@ SELECT_SETTINGS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add kostal plenticore Select widget."""
|
||||
plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
available_settings_data = await plenticore.client.get_settings()
|
||||
select_data_update_coordinator = SelectDataUpdateCoordinator(
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
@@ -29,8 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ProcessDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, ProcessDataUpdateCoordinator
|
||||
from .helper import PlenticoreDataFormatter
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -808,11 +806,11 @@ SENSOR_PROCESS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add kostal plenticore Sensors."""
|
||||
plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
|
||||
@@ -8,15 +8,13 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SettingDataUpdateCoordinator
|
||||
from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,11 +47,11 @@ SWITCH_SETTINGS_DATA = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PlenticoreConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add kostal plenticore Switch."""
|
||||
plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
plenticore = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from thinqconnect import DeviceType
|
||||
from thinqconnect.integration import ExtendedProperty
|
||||
@@ -154,7 +155,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
|
||||
)
|
||||
)
|
||||
|
||||
async def async_return_to_base(self, **kwargs) -> None:
|
||||
async def async_return_to_base(self, **kwargs: Any) -> None:
|
||||
"""Return device to dock."""
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] async_return_to_base",
|
||||
|
||||
@@ -354,8 +354,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
def write_dump() -> None:
|
||||
with open(hass.config.path("mqtt_dump.txt"), "w", encoding="utf8") as fp:
|
||||
for msg in messages:
|
||||
fp.write(",".join(msg) + "\n")
|
||||
fp.writelines([",".join(msg) + "\n" for msg in messages])
|
||||
|
||||
async def finish_dump(_: datetime) -> None:
|
||||
"""Write dump to file."""
|
||||
@@ -608,8 +607,7 @@ async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove MQTT config entry from a device."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_automation
|
||||
from . import device_automation # noqa: PLC0415
|
||||
|
||||
await device_automation.async_removed_from_device(hass, device_entry.id)
|
||||
return True
|
||||
|
||||
@@ -293,10 +293,9 @@ class MqttClientSetup:
|
||||
"""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
from paho.mqtt import client as mqtt # pylint: disable=import-outside-toplevel
|
||||
from paho.mqtt import client as mqtt # noqa: PLC0415
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .async_client import AsyncMQTTClient
|
||||
from .async_client import AsyncMQTTClient # noqa: PLC0415
|
||||
|
||||
config = self._config
|
||||
clean_session: bool | None = None
|
||||
@@ -524,8 +523,7 @@ class MQTT:
|
||||
"""Start the misc periodic."""
|
||||
assert self._misc_timer is None, "Misc periodic already started"
|
||||
_LOGGER.debug("%s: Starting client misc loop", self.config_entry.title)
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
# Inner function to avoid having to check late import
|
||||
# each time the function is called.
|
||||
@@ -665,8 +663,7 @@ class MQTT:
|
||||
|
||||
async def async_connect(self, client_available: asyncio.Future[bool]) -> None:
|
||||
"""Connect to the host. Does not process messages yet."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
result: int | None = None
|
||||
self._available_future = client_available
|
||||
@@ -724,8 +721,7 @@ class MQTT:
|
||||
|
||||
async def _reconnect_loop(self) -> None:
|
||||
"""Reconnect to the MQTT server."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
while True:
|
||||
if not self.connected:
|
||||
@@ -1228,7 +1224,7 @@ class MQTT:
|
||||
"""Handle a callback exception."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
_LOGGER.warning(
|
||||
"Error returned from MQTT server: %s",
|
||||
@@ -1273,8 +1269,7 @@ class MQTT:
|
||||
) -> None:
|
||||
"""Wait for ACK from broker or raise on error."""
|
||||
if result_code != 0:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -1322,8 +1317,7 @@ class MQTT:
|
||||
|
||||
|
||||
def _matcher_for_topic(subscription: str) -> Callable[[str], bool]:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from paho.mqtt.matcher import MQTTMatcher
|
||||
from paho.mqtt.matcher import MQTTMatcher # noqa: PLC0415
|
||||
|
||||
matcher = MQTTMatcher() # type: ignore[no-untyped-call]
|
||||
matcher[subscription] = True
|
||||
|
||||
@@ -3493,7 +3493,7 @@ def try_connection(
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
mqtt_client_setup = MqttClientSetup(user_input)
|
||||
mqtt_client_setup.setup()
|
||||
|
||||
@@ -640,8 +640,7 @@ async def cleanup_device_registry(
|
||||
entities, triggers or tags.
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_trigger, tag
|
||||
from . import device_trigger, tag # noqa: PLC0415
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -163,16 +163,14 @@ async def async_forward_entry_setup_and_setup_discovery(
|
||||
tasks: list[asyncio.Task] = []
|
||||
if "device_automation" in new_platforms:
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_automation
|
||||
from . import device_automation # noqa: PLC0415
|
||||
|
||||
tasks.append(
|
||||
create_eager_task(device_automation.async_setup_entry(hass, config_entry))
|
||||
)
|
||||
if "tag" in new_platforms:
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import tag
|
||||
from . import tag # noqa: PLC0415
|
||||
|
||||
tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry)))
|
||||
if new_entity_platforms := (new_platforms - {"tag", "device_automation"}):
|
||||
|
||||
@@ -175,9 +175,7 @@ async def async_get_announce_addresses(hass: HomeAssistant) -> list[str]:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up network for Home Assistant."""
|
||||
# Avoid circular issue: http->network->websocket_api->http
|
||||
from .websocket import ( # pylint: disable=import-outside-toplevel
|
||||
async_register_websocket_commands,
|
||||
)
|
||||
from .websocket import async_register_websocket_commands # noqa: PLC0415
|
||||
|
||||
await async_get_network(hass)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import (
|
||||
@@ -33,10 +33,10 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData)
|
||||
|
||||
|
||||
class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
class NextDnsUpdateCoordinator[CoordinatorDataT: NextDnsData](
|
||||
DataUpdateCoordinator[CoordinatorDataT]
|
||||
):
|
||||
"""Class to manage fetching NextDNS data API."""
|
||||
|
||||
config_entry: NextDnsConfigEntry
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""Define NextDNS entities."""
|
||||
|
||||
from nextdns.model import NextDnsData
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
|
||||
|
||||
class NextDnsEntity(CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]]):
|
||||
class NextDnsEntity[CoordinatorDataT: NextDnsData](
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]]
|
||||
):
|
||||
"""Define NextDNS entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from nextdns import (
|
||||
AnalyticsDnssec,
|
||||
@@ -13,6 +12,7 @@ from nextdns import (
|
||||
AnalyticsProtocols,
|
||||
AnalyticsStatus,
|
||||
)
|
||||
from nextdns.model import NextDnsData
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -32,15 +32,14 @@ from .const import (
|
||||
ATTR_PROTOCOLS,
|
||||
ATTR_STATUS,
|
||||
)
|
||||
from .coordinator import CoordinatorDataT
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NextDnsSensorEntityDescription(
|
||||
SensorEntityDescription, Generic[CoordinatorDataT]
|
||||
class NextDnsSensorEntityDescription[CoordinatorDataT: NextDnsData](
|
||||
SensorEntityDescription
|
||||
):
|
||||
"""NextDNS sensor entity description."""
|
||||
|
||||
@@ -297,10 +296,12 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsSensor(NextDnsEntity, SensorEntity):
|
||||
class NextDnsSensor[CoordinatorDataT: NextDnsData](
|
||||
NextDnsEntity[CoordinatorDataT], SensorEntity
|
||||
):
|
||||
"""Define an NextDNS sensor."""
|
||||
|
||||
entity_description: NextDnsSensorEntityDescription
|
||||
entity_description: NextDnsSensorEntityDescription[CoordinatorDataT]
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
|
||||
@@ -282,8 +282,7 @@ class BaseNotificationService:
|
||||
|
||||
for name, target in self.targets.items():
|
||||
target_name = slugify(f"{self._target_service_name_prefix}_{name}")
|
||||
if target_name in stale_targets:
|
||||
stale_targets.remove(target_name)
|
||||
stale_targets.discard(target_name)
|
||||
if (
|
||||
target_name in self.registered_targets
|
||||
and target == self.registered_targets[target_name]
|
||||
|
||||
@@ -322,8 +322,9 @@ class OllamaConversationEntity(
|
||||
num_keep = 2 * max_messages + 1
|
||||
drop_index = len(message_history.messages) - num_keep
|
||||
message_history.messages = [
|
||||
message_history.messages[0]
|
||||
] + message_history.messages[drop_index:]
|
||||
message_history.messages[0],
|
||||
*message_history.messages[drop_index:],
|
||||
]
|
||||
|
||||
async def _async_entry_update_listener(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry
|
||||
|
||||
@@ -218,8 +218,7 @@ class UserOnboardingView(_BaseOnboardingStepView):
|
||||
|
||||
# Return authorization code for fetching tokens and connect
|
||||
# during onboarding.
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.auth import create_auth_code
|
||||
from homeassistant.components.auth import create_auth_code # noqa: PLC0415
|
||||
|
||||
auth_code = create_auth_code(hass, data["client_id"], credentials)
|
||||
return self.json({"auth_code": auth_code})
|
||||
@@ -309,8 +308,7 @@ class IntegrationOnboardingView(_BaseOnboardingStepView):
|
||||
)
|
||||
|
||||
# Return authorization code so we can redirect user and log them in
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.auth import create_auth_code
|
||||
from homeassistant.components.auth import create_auth_code # noqa: PLC0415
|
||||
|
||||
auth_code = create_auth_code(
|
||||
hass, data["client_id"], refresh_token.credential
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user