mirror of
https://github.com/home-assistant/core.git
synced 2026-01-02 04:01:54 +01:00
Compare commits
3 Commits
setpoint_c
...
adjust_sen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccbd681d20 | ||
|
|
01aa70e249 | ||
|
|
9f6a0c0c77 |
12
.github/workflows/builder.yml
vendored
12
.github/workflows/builder.yml
vendored
@@ -190,8 +190,7 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- &install_cosign
|
||||
name: Install Cosign
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
@@ -295,7 +294,7 @@ jobs:
|
||||
|
||||
# home-assistant/builder doesn't support sha pinning
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2025.11.0
|
||||
uses: home-assistant/builder@2025.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -354,7 +353,10 @@ jobs:
|
||||
matrix:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- *install_cosign
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
@@ -391,7 +393,7 @@ jobs:
|
||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: ${{ matrix.registry }}/home-assistant
|
||||
sep-tags: ","
|
||||
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@02c6cc30ae592ce65ee356387748dfc2fd5f7993 # v2.0.3
|
||||
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@02c6cc30ae592ce65ee356387748dfc2fd5f7993 # v2.0.3
|
||||
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
||||
@@ -35,22 +35,25 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
USER vscode
|
||||
|
||||
COPY .python-version ./
|
||||
RUN uv python install
|
||||
|
||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||
RUN --mount=type=bind,source=.python-version,target=.python-version \
|
||||
uv python install \
|
||||
&& uv venv $VIRTUAL_ENV
|
||||
RUN uv venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||
&& uv pip install -e ~/hass-release/
|
||||
|
||||
# Install Python dependencies from requirements
|
||||
RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
--mount=type=bind,source=homeassistant/package_constraints.txt,target=homeassistant/package_constraints.txt \
|
||||
--mount=type=bind,source=requirements_test.txt,target=requirements_test.txt \
|
||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
COPY requirements.txt ./
|
||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||
RUN uv pip install -r requirements.txt
|
||||
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||
RUN uv pip install -r requirements_test.txt
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
|
||||
@@ -1000,7 +1000,7 @@ class _WatchPendingSetups:
|
||||
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
||||
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
||||
_LOGGER.warning(
|
||||
"Waiting for integrations to complete setup: %s",
|
||||
"Waiting on integrations to complete setup: %s",
|
||||
self._setup_started,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from pyanglianwater import AnglianWater
|
||||
from pyanglianwater.auth import MSOB2CAuth
|
||||
from pyanglianwater.exceptions import (
|
||||
@@ -19,7 +18,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
|
||||
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
|
||||
@@ -34,10 +33,7 @@ async def async_setup_entry(
|
||||
auth = MSOB2CAuth(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=async_create_clientsession(
|
||||
hass,
|
||||
cookie_jar=CookieJar(quote_cookie=False),
|
||||
),
|
||||
session=async_get_clientsession(hass),
|
||||
refresh_token=entry.data[CONF_ACCESS_TOKEN],
|
||||
account_number=entry.data[CONF_ACCOUNT_NUMBER],
|
||||
)
|
||||
|
||||
@@ -18,21 +18,17 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
|
||||
"""Defines a Anglian Water entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AnglianWaterUpdateCoordinator,
|
||||
smart_meter: SmartMeter,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Initialize Anglian Water entity."""
|
||||
super().__init__(coordinator)
|
||||
self.smart_meter = smart_meter
|
||||
self._attr_unique_id = f"{smart_meter.serial_number}_{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, smart_meter.serial_number)},
|
||||
name=smart_meter.serial_number,
|
||||
name="Smart Water Meter",
|
||||
manufacturer="Anglian Water",
|
||||
serial_number=smart_meter.serial_number,
|
||||
)
|
||||
|
||||
@@ -108,8 +108,9 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
|
||||
description: AnglianWaterSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Anglian Water sensor."""
|
||||
super().__init__(coordinator, smart_meter, description.key)
|
||||
super().__init__(coordinator, smart_meter)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{smart_meter.serial_number}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
|
||||
from .const import CONF_CHAT_MODEL, DEFAULT, DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
|
||||
|
||||
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -37,7 +37,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
|
||||
)
|
||||
try:
|
||||
await client.models.list(timeout=10.0)
|
||||
# Use model from first conversation subentry for validation
|
||||
subentries = list(entry.subentries.values())
|
||||
if subentries:
|
||||
model_id = subentries[0].data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
else:
|
||||
model_id = DEFAULT[CONF_CHAT_MODEL]
|
||||
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
|
||||
LOGGER.debug("Anthropic model: %s", model.display_name)
|
||||
except anthropic.AuthenticationError as err:
|
||||
LOGGER.error("Invalid API key: %s", err)
|
||||
return False
|
||||
|
||||
@@ -421,8 +421,6 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
)
|
||||
if short_form.search(model_alias):
|
||||
model_alias += "-0"
|
||||
if model_alias.endswith(("haiku", "opus", "sonnet")):
|
||||
model_alias += "-latest"
|
||||
model_options.append(
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
|
||||
@@ -583,7 +583,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
name=subentry.title,
|
||||
manufacturer="Anthropic",
|
||||
model=subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL]),
|
||||
model="Claude",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["anthropic==0.75.0"]
|
||||
"requirements": ["anthropic==0.73.0"]
|
||||
}
|
||||
|
||||
@@ -1123,6 +1123,63 @@ class PipelineRun:
|
||||
)
|
||||
|
||||
try:
|
||||
user_input = conversation.ConversationInput(
|
||||
text=intent_input,
|
||||
context=self.context,
|
||||
conversation_id=conversation_id,
|
||||
device_id=self._device_id,
|
||||
satellite_id=self._satellite_id,
|
||||
language=input_language,
|
||||
agent_id=self.intent_agent.id,
|
||||
extra_system_prompt=conversation_extra_system_prompt,
|
||||
)
|
||||
|
||||
agent_id = self.intent_agent.id
|
||||
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
|
||||
all_targets_in_satellite_area = False
|
||||
intent_response: intent.IntentResponse | None = None
|
||||
if not processed_locally and not self._intent_agent_only:
|
||||
# Sentence triggers override conversation agent
|
||||
if (
|
||||
trigger_response_text
|
||||
:= await conversation.async_handle_sentence_triggers(
|
||||
self.hass, user_input
|
||||
)
|
||||
) is not None:
|
||||
# Sentence trigger matched
|
||||
agent_id = "sentence_trigger"
|
||||
processed_locally = True
|
||||
intent_response = intent.IntentResponse(
|
||||
self.pipeline.conversation_language
|
||||
)
|
||||
intent_response.async_set_speech(trigger_response_text)
|
||||
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None
|
||||
# If the LLM has API access, we filter out some sentences that are
|
||||
# interfering with LLM operation.
|
||||
if (
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent.id)
|
||||
) and intent_agent_state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
) & conversation.ConversationEntityFeature.CONTROL:
|
||||
intent_filter = _async_local_fallback_intent_filter
|
||||
|
||||
# Try local intents
|
||||
if (
|
||||
intent_response is None
|
||||
and self.pipeline.prefer_local_intents
|
||||
and (
|
||||
intent_response := await conversation.async_handle_intents(
|
||||
self.hass,
|
||||
user_input,
|
||||
intent_filter=intent_filter,
|
||||
)
|
||||
)
|
||||
):
|
||||
# Local intent matched
|
||||
agent_id = conversation.HOME_ASSISTANT_AGENT
|
||||
processed_locally = True
|
||||
|
||||
if self.tts_stream and self.tts_stream.supports_streaming_input:
|
||||
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
|
||||
else:
|
||||
@@ -1208,17 +1265,6 @@ class PipelineRun:
|
||||
assert self.tts_stream is not None
|
||||
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
|
||||
|
||||
user_input = conversation.ConversationInput(
|
||||
text=intent_input,
|
||||
context=self.context,
|
||||
conversation_id=conversation_id,
|
||||
device_id=self._device_id,
|
||||
satellite_id=self._satellite_id,
|
||||
language=input_language,
|
||||
agent_id=self.intent_agent.id,
|
||||
extra_system_prompt=conversation_extra_system_prompt,
|
||||
)
|
||||
|
||||
with (
|
||||
chat_session.async_get_chat_session(
|
||||
self.hass, user_input.conversation_id
|
||||
@@ -1230,53 +1276,6 @@ class PipelineRun:
|
||||
chat_log_delta_listener=chat_log_delta_listener,
|
||||
) as chat_log,
|
||||
):
|
||||
agent_id = self.intent_agent.id
|
||||
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
|
||||
all_targets_in_satellite_area = False
|
||||
intent_response: intent.IntentResponse | None = None
|
||||
if not processed_locally and not self._intent_agent_only:
|
||||
# Sentence triggers override conversation agent
|
||||
if (
|
||||
trigger_response_text
|
||||
:= await conversation.async_handle_sentence_triggers(
|
||||
self.hass, user_input, chat_log
|
||||
)
|
||||
) is not None:
|
||||
# Sentence trigger matched
|
||||
agent_id = "sentence_trigger"
|
||||
processed_locally = True
|
||||
intent_response = intent.IntentResponse(
|
||||
self.pipeline.conversation_language
|
||||
)
|
||||
intent_response.async_set_speech(trigger_response_text)
|
||||
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None
|
||||
# If the LLM has API access, we filter out some sentences that are
|
||||
# interfering with LLM operation.
|
||||
if (
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent.id)
|
||||
) and intent_agent_state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
) & conversation.ConversationEntityFeature.CONTROL:
|
||||
intent_filter = _async_local_fallback_intent_filter
|
||||
|
||||
# Try local intents
|
||||
if (
|
||||
intent_response is None
|
||||
and self.pipeline.prefer_local_intents
|
||||
and (
|
||||
intent_response := await conversation.async_handle_intents(
|
||||
self.hass,
|
||||
user_input,
|
||||
chat_log,
|
||||
intent_filter=intent_filter,
|
||||
)
|
||||
)
|
||||
):
|
||||
# Local intent matched
|
||||
agent_id = conversation.HOME_ASSISTANT_AGENT
|
||||
processed_locally = True
|
||||
|
||||
# It was already handled, create response and add to chat history
|
||||
if intent_response is not None:
|
||||
speech: str = intent_response.speech.get("plain", {}).get(
|
||||
|
||||
@@ -17,12 +17,8 @@ from homeassistant.components.media_player import (
|
||||
class BangOlufsenSource:
|
||||
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
||||
|
||||
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
|
||||
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
||||
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
|
||||
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
||||
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
|
||||
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
|
||||
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
||||
|
||||
|
||||
@@ -82,16 +78,6 @@ class BangOlufsenModel(StrEnum):
|
||||
BEOREMOTE_ONE = "Beoremote One"
|
||||
|
||||
|
||||
class BangOlufsenAttribute(StrEnum):
|
||||
"""Enum for extra_state_attribute keys."""
|
||||
|
||||
BEOLINK = "beolink"
|
||||
BEOLINK_PEERS = "peers"
|
||||
BEOLINK_SELF = "self"
|
||||
BEOLINK_LEADER = "leader"
|
||||
BEOLINK_LISTENERS = "listeners"
|
||||
|
||||
|
||||
# Physical "buttons" on devices
|
||||
class BangOlufsenButtons(StrEnum):
|
||||
"""Enum for device buttons."""
|
||||
|
||||
@@ -82,7 +82,6 @@ from .const import (
|
||||
FALLBACK_SOURCES,
|
||||
MANUFACTURER,
|
||||
VALID_MEDIA_TYPES,
|
||||
BangOlufsenAttribute,
|
||||
BangOlufsenMediaType,
|
||||
BangOlufsenSource,
|
||||
WebsocketNotification,
|
||||
@@ -225,8 +224,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
# Beolink compatible sources
|
||||
self._beolink_sources: dict[str, bool] = {}
|
||||
self._remote_leader: BeolinkLeader | None = None
|
||||
# Extra state attributes:
|
||||
# Beolink: peer(s), listener(s), leader and self
|
||||
# Extra state attributes for showing Beolink: peer(s), listener(s), leader and self
|
||||
self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -438,10 +436,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
await self._async_update_beolink()
|
||||
|
||||
async def _async_update_beolink(self) -> None:
|
||||
"""Update the current Beolink leader, listeners, peers and self.
|
||||
|
||||
Updates Home Assistant state.
|
||||
"""
|
||||
"""Update the current Beolink leader, listeners, peers and self."""
|
||||
|
||||
self._beolink_attributes = {}
|
||||
|
||||
@@ -450,24 +445,18 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
# Add Beolink self
|
||||
self._beolink_attributes = {
|
||||
BangOlufsenAttribute.BEOLINK: {
|
||||
BangOlufsenAttribute.BEOLINK_SELF: {
|
||||
self.device_entry.name: self._beolink_jid
|
||||
}
|
||||
}
|
||||
"beolink": {"self": {self.device_entry.name: self._beolink_jid}}
|
||||
}
|
||||
|
||||
# Add Beolink peers
|
||||
peers = await self._client.get_beolink_peers()
|
||||
|
||||
if len(peers) > 0:
|
||||
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
|
||||
BangOlufsenAttribute.BEOLINK_PEERS
|
||||
] = {}
|
||||
self._beolink_attributes["beolink"]["peers"] = {}
|
||||
for peer in peers:
|
||||
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
|
||||
BangOlufsenAttribute.BEOLINK_PEERS
|
||||
][peer.friendly_name] = peer.jid
|
||||
self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = (
|
||||
peer.jid
|
||||
)
|
||||
|
||||
# Add Beolink listeners / leader
|
||||
self._remote_leader = self._playback_metadata.remote_leader
|
||||
@@ -488,9 +477,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
# Add self
|
||||
group_members.append(self.entity_id)
|
||||
|
||||
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
|
||||
BangOlufsenAttribute.BEOLINK_LEADER
|
||||
] = {
|
||||
self._beolink_attributes["beolink"]["leader"] = {
|
||||
self._remote_leader.friendly_name: self._remote_leader.jid,
|
||||
}
|
||||
|
||||
@@ -527,9 +514,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
beolink_listener.jid
|
||||
)
|
||||
break
|
||||
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
|
||||
BangOlufsenAttribute.BEOLINK_LISTENERS
|
||||
] = beolink_listeners_attribute
|
||||
self._beolink_attributes["beolink"]["listeners"] = (
|
||||
beolink_listeners_attribute
|
||||
)
|
||||
|
||||
self._attr_group_members = group_members
|
||||
|
||||
@@ -628,18 +615,11 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_content_type(self) -> MediaType | str | None:
|
||||
def media_content_type(self) -> str:
|
||||
"""Return the current media type."""
|
||||
content_type = {
|
||||
BangOlufsenSource.URI_STREAMER.id: MediaType.URL,
|
||||
BangOlufsenSource.DEEZER.id: BangOlufsenMediaType.DEEZER,
|
||||
BangOlufsenSource.TIDAL.id: BangOlufsenMediaType.TIDAL,
|
||||
BangOlufsenSource.NET_RADIO.id: BangOlufsenMediaType.RADIO,
|
||||
}
|
||||
# Hard to determine content type.
|
||||
if self._source_change.id in content_type:
|
||||
return content_type[self._source_change.id]
|
||||
|
||||
# Hard to determine content type
|
||||
if self._source_change.id == BangOlufsenSource.URI_STREAMER.id:
|
||||
return MediaType.URL
|
||||
return MediaType.MUSIC
|
||||
|
||||
@property
|
||||
@@ -652,11 +632,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
"""Return the current playback progress."""
|
||||
return self._playback_progress.progress
|
||||
|
||||
@property
|
||||
def media_content_id(self) -> str | None:
|
||||
"""Return internal ID of Deezer, Tidal and radio stations."""
|
||||
return self._playback_metadata.source_internal_id
|
||||
|
||||
@property
|
||||
def media_image_url(self) -> str | None:
|
||||
"""Return URL of the currently playing music."""
|
||||
|
||||
@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
|
||||
config_entry_id=entry.entry_id,
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
|
||||
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
|
||||
name=f"Bosch {panel.model.name}",
|
||||
name=f"Bosch {panel.model}",
|
||||
manufacturer="Bosch Security Systems",
|
||||
model=panel.model.name,
|
||||
model=panel.model,
|
||||
sw_version=panel.firmware_version,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -83,7 +83,7 @@ async def try_connect(
|
||||
finally:
|
||||
await panel.disconnect()
|
||||
|
||||
return (panel.model.name, panel.serial_number)
|
||||
return (panel.model, panel.serial_number)
|
||||
|
||||
|
||||
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@@ -20,8 +20,7 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": {
|
||||
"model": entry.runtime_data.model.name,
|
||||
"family": entry.runtime_data.model.family.name,
|
||||
"model": entry.runtime_data.model,
|
||||
"serial_number": entry.runtime_data.serial_number,
|
||||
"protocol_version": entry.runtime_data.protocol_version,
|
||||
"firmware_version": entry.runtime_data.firmware_version,
|
||||
|
||||
@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
|
||||
self._attr_should_poll = False
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
name=f"Bosch {panel.model.name}",
|
||||
name=f"Bosch {panel.model}",
|
||||
manufacturer="Bosch Security Systems",
|
||||
)
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bosch-alarm-mode2==0.4.10"]
|
||||
"requirements": ["bosch-alarm-mode2==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -98,12 +98,6 @@
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"started_cooling": {
|
||||
"trigger": "mdi:snowflake"
|
||||
},
|
||||
"started_drying": {
|
||||
"trigger": "mdi:water-percent"
|
||||
},
|
||||
"started_heating": {
|
||||
"trigger": "mdi:fire"
|
||||
},
|
||||
|
||||
@@ -298,28 +298,6 @@
|
||||
},
|
||||
"title": "Climate",
|
||||
"triggers": {
|
||||
"started_cooling": {
|
||||
"description": "Triggers when a climate started cooling.",
|
||||
"description_configured": "[%key:component::climate::triggers::started_cooling::description%]",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "When a climate started cooling"
|
||||
},
|
||||
"started_drying": {
|
||||
"description": "Triggers when a climate started drying.",
|
||||
"description_configured": "[%key:component::climate::triggers::started_drying::description%]",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "When a climate started drying"
|
||||
},
|
||||
"started_heating": {
|
||||
"description": "Triggers when a climate starts to heat.",
|
||||
"description_configured": "[%key:component::climate::triggers::started_heating::description%]",
|
||||
|
||||
@@ -11,12 +11,6 @@ from homeassistant.helpers.trigger import (
|
||||
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"started_cooling": make_entity_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
|
||||
),
|
||||
"started_drying": make_entity_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
|
||||
),
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_conditional_entity_state_trigger(
|
||||
DOMAIN,
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
started_cooling: *trigger_common
|
||||
started_drying: *trigger_common
|
||||
started_heating: *trigger_common
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -6,7 +6,6 @@ import io
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
|
||||
from hass_nabucasa import NabuCasaBaseError
|
||||
from hass_nabucasa.llm import (
|
||||
LLMAuthenticationError,
|
||||
LLMError,
|
||||
@@ -94,11 +93,10 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Home Assistant Cloud AI Task entity."""
|
||||
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
|
||||
return
|
||||
cloud = hass.data[DATA_CLOUD]
|
||||
try:
|
||||
await cloud.llm.async_ensure_token()
|
||||
except (LLMError, NabuCasaBaseError):
|
||||
except LLMError:
|
||||
return
|
||||
|
||||
async_add_entities([CloudLLMTaskEntity(cloud, config_entry)])
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from hass_nabucasa import NabuCasaBaseError
|
||||
from hass_nabucasa.llm import LLMError
|
||||
|
||||
from homeassistant.components import conversation
|
||||
@@ -24,11 +23,10 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Home Assistant Cloud conversation entity."""
|
||||
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
|
||||
return
|
||||
cloud = hass.data[DATA_CLOUD]
|
||||
try:
|
||||
await cloud.llm.async_ensure_token()
|
||||
except (LLMError, NabuCasaBaseError):
|
||||
except LLMError:
|
||||
return
|
||||
|
||||
async_add_entities([CloudConversationEntity(cloud, config_entry)])
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.6.2"],
|
||||
"requirements": ["hass-nabucasa==1.6.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -236,9 +236,7 @@ async def async_prepare_agent(
|
||||
|
||||
|
||||
async def async_handle_sentence_triggers(
|
||||
hass: HomeAssistant,
|
||||
user_input: ConversationInput,
|
||||
chat_log: ChatLog,
|
||||
hass: HomeAssistant, user_input: ConversationInput
|
||||
) -> str | None:
|
||||
"""Try to match input against sentence triggers and return response text.
|
||||
|
||||
@@ -247,13 +245,12 @@ async def async_handle_sentence_triggers(
|
||||
agent = get_agent_manager(hass).default_agent
|
||||
assert agent is not None
|
||||
|
||||
return await agent.async_handle_sentence_triggers(user_input, chat_log)
|
||||
return await agent.async_handle_sentence_triggers(user_input)
|
||||
|
||||
|
||||
async def async_handle_intents(
|
||||
hass: HomeAssistant,
|
||||
user_input: ConversationInput,
|
||||
chat_log: ChatLog,
|
||||
*,
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None,
|
||||
) -> intent.IntentResponse | None:
|
||||
@@ -264,9 +261,7 @@ async def async_handle_intents(
|
||||
agent = get_agent_manager(hass).default_agent
|
||||
assert agent is not None
|
||||
|
||||
return await agent.async_handle_intents(
|
||||
user_input, chat_log, intent_filter=intent_filter
|
||||
)
|
||||
return await agent.async_handle_intents(user_input, intent_filter=intent_filter)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
@@ -66,7 +66,6 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
llm,
|
||||
start as ha_start,
|
||||
template,
|
||||
translation,
|
||||
@@ -77,7 +76,7 @@ from homeassistant.util import language as language_util
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .agent_manager import get_agent_manager
|
||||
from .chat_log import AssistantContent, ChatLog, ToolResultContent
|
||||
from .chat_log import AssistantContent, ChatLog
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
METADATA_CUSTOM_FILE,
|
||||
@@ -436,7 +435,7 @@ class DefaultAgent(ConversationEntity):
|
||||
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
|
||||
# Process callbacks and get response
|
||||
response_text = await self._handle_trigger_result(
|
||||
trigger_result, user_input, chat_log
|
||||
trigger_result, user_input
|
||||
)
|
||||
|
||||
# Convert to conversation result
|
||||
@@ -448,9 +447,8 @@ class DefaultAgent(ConversationEntity):
|
||||
if response is None:
|
||||
# Match intents
|
||||
intent_result = await self.async_recognize_intent(user_input)
|
||||
|
||||
response = await self._async_process_intent_result(
|
||||
intent_result, user_input, chat_log
|
||||
intent_result, user_input
|
||||
)
|
||||
|
||||
speech: str = response.speech.get("plain", {}).get("speech", "")
|
||||
@@ -469,7 +467,6 @@ class DefaultAgent(ConversationEntity):
|
||||
self,
|
||||
result: RecognizeResult | None,
|
||||
user_input: ConversationInput,
|
||||
chat_log: ChatLog,
|
||||
) -> intent.IntentResponse:
|
||||
"""Process user input with intents."""
|
||||
language = user_input.language or self.hass.config.language
|
||||
@@ -532,21 +529,12 @@ class DefaultAgent(ConversationEntity):
|
||||
ConversationTraceEventType.TOOL_CALL,
|
||||
{
|
||||
"intent_name": result.intent.name,
|
||||
"slots": {entity.name: entity.value for entity in result.entities_list},
|
||||
"slots": {
|
||||
entity.name: entity.value or entity.text
|
||||
for entity in result.entities_list
|
||||
},
|
||||
},
|
||||
)
|
||||
tool_input = llm.ToolInput(
|
||||
tool_name=result.intent.name,
|
||||
tool_args={entity.name: entity.value for entity in result.entities_list},
|
||||
external=True,
|
||||
)
|
||||
chat_log.async_add_assistant_content_without_tools(
|
||||
AssistantContent(
|
||||
agent_id=user_input.agent_id,
|
||||
content=None,
|
||||
tool_calls=[tool_input],
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
intent_response = await intent.async_handle(
|
||||
@@ -609,16 +597,6 @@ class DefaultAgent(ConversationEntity):
|
||||
)
|
||||
intent_response.async_set_speech(speech)
|
||||
|
||||
tool_result = llm.IntentResponseDict(intent_response)
|
||||
chat_log.async_add_assistant_content_without_tools(
|
||||
ToolResultContent(
|
||||
agent_id=user_input.agent_id,
|
||||
tool_call_id=tool_input.id,
|
||||
tool_name=tool_input.tool_name,
|
||||
tool_result=tool_result,
|
||||
)
|
||||
)
|
||||
|
||||
return intent_response
|
||||
|
||||
def _recognize(
|
||||
@@ -1545,31 +1523,16 @@ class DefaultAgent(ConversationEntity):
|
||||
)
|
||||
|
||||
async def _handle_trigger_result(
|
||||
self,
|
||||
result: SentenceTriggerResult,
|
||||
user_input: ConversationInput,
|
||||
chat_log: ChatLog,
|
||||
self, result: SentenceTriggerResult, user_input: ConversationInput
|
||||
) -> str:
|
||||
"""Run sentence trigger callbacks and return response text."""
|
||||
|
||||
# Gather callback responses in parallel
|
||||
trigger_callbacks = [
|
||||
self._triggers_details[trigger_id].callback(user_input, trigger_result)
|
||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||
]
|
||||
|
||||
tool_input = llm.ToolInput(
|
||||
tool_name="trigger_sentence",
|
||||
tool_args={},
|
||||
external=True,
|
||||
)
|
||||
chat_log.async_add_assistant_content_without_tools(
|
||||
AssistantContent(
|
||||
agent_id=user_input.agent_id,
|
||||
content=None,
|
||||
tool_calls=[tool_input],
|
||||
)
|
||||
)
|
||||
|
||||
# Use first non-empty result as response.
|
||||
#
|
||||
# There may be multiple copies of a trigger running when editing in
|
||||
@@ -1598,38 +1561,23 @@ class DefaultAgent(ConversationEntity):
|
||||
f"component.{DOMAIN}.conversation.agent.done", "Done"
|
||||
)
|
||||
|
||||
tool_result: dict[str, Any] = {"response": response_text}
|
||||
chat_log.async_add_assistant_content_without_tools(
|
||||
ToolResultContent(
|
||||
agent_id=user_input.agent_id,
|
||||
tool_call_id=tool_input.id,
|
||||
tool_name=tool_input.tool_name,
|
||||
tool_result=tool_result,
|
||||
)
|
||||
)
|
||||
|
||||
return response_text
|
||||
|
||||
async def async_handle_sentence_triggers(
|
||||
self,
|
||||
user_input: ConversationInput,
|
||||
chat_log: ChatLog,
|
||||
self, user_input: ConversationInput
|
||||
) -> str | None:
|
||||
"""Try to input sentence against sentence triggers and return response text.
|
||||
|
||||
Returns None if no match occurred.
|
||||
"""
|
||||
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
|
||||
return await self._handle_trigger_result(
|
||||
trigger_result, user_input, chat_log
|
||||
)
|
||||
return await self._handle_trigger_result(trigger_result, user_input)
|
||||
|
||||
return None
|
||||
|
||||
async def async_handle_intents(
|
||||
self,
|
||||
user_input: ConversationInput,
|
||||
chat_log: ChatLog,
|
||||
*,
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None,
|
||||
) -> intent.IntentResponse | None:
|
||||
@@ -1645,7 +1593,7 @@ class DefaultAgent(ConversationEntity):
|
||||
# No error message on failed match
|
||||
return None
|
||||
|
||||
response = await self._async_process_intent_result(result, user_input, chat_log)
|
||||
response = await self._async_process_intent_result(result, user_input)
|
||||
if (
|
||||
response.response_type == intent.IntentResponseType.ERROR
|
||||
and response.error_code
|
||||
|
||||
@@ -8,10 +8,6 @@ from typing import Any
|
||||
from pycoolmasternet_async import SWING_MODES
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
@@ -35,16 +31,7 @@ CM_TO_HA_STATE = {
|
||||
|
||||
HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()}
|
||||
|
||||
CM_TO_HA_FAN = {
|
||||
"low": FAN_LOW,
|
||||
"med": FAN_MEDIUM,
|
||||
"high": FAN_HIGH,
|
||||
"auto": FAN_AUTO,
|
||||
}
|
||||
|
||||
HA_FAN_TO_CM = {value: key for key, value in CM_TO_HA_FAN.items()}
|
||||
|
||||
FAN_MODES = list(CM_TO_HA_FAN.values())
|
||||
FAN_MODES = ["low", "med", "high", "auto"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -124,7 +111,7 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity):
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return CM_TO_HA_FAN[self._unit.fan_speed]
|
||||
return self._unit.fan_speed
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
@@ -151,7 +138,7 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity):
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new fan mode."""
|
||||
_LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, fan_mode)
|
||||
self._unit = await self._unit.set_fan_speed(HA_FAN_TO_CM[fan_mode])
|
||||
self._unit = await self._unit.set_fan_speed(fan_mode)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
|
||||
@@ -285,14 +285,16 @@ async def async_setup_entry(
|
||||
name=sensor.name,
|
||||
)
|
||||
|
||||
# Only total rain needs state class for long-term statistics
|
||||
# Hourly rain doesn't reset to fixed hours, it must be measurement state classes
|
||||
if sensor.key in (
|
||||
"totalrainin",
|
||||
"totalrainmm",
|
||||
"hrain_piezomm",
|
||||
"hrain_piezo",
|
||||
"hourlyrainmm",
|
||||
"hourlyrainin",
|
||||
):
|
||||
description = dataclasses.replace(
|
||||
description,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
)
|
||||
|
||||
async_add_entities([EcowittSensorEntity(sensor, description)])
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.4.2"],
|
||||
"requirements": ["pyenphase==2.4.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.11.0"
|
||||
STABLE_BLE_VERSION_STR = "2025.8.0"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==42.9.0",
|
||||
"aioesphomeapi==42.8.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.4.0"
|
||||
],
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
"title": "[%key:component::assist_pipeline::issues::assist_in_progress_deprecated::title%]"
|
||||
},
|
||||
"ble_firmware_outdated": {
|
||||
"description": "ESPHome {version} introduces ultra-low latency event processing, reducing BLE event delays from 0-16 milliseconds to approximately 12 microseconds. This resolves stability issues when pairing, connecting, or handshaking with devices that require low latency, and makes Bluetooth proxy operations rival or exceed local adapters. We highly recommend updating {name} to take advantage of these improvements.",
|
||||
"description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device from ESPHome earlier than 2022.12.0, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme.",
|
||||
"title": "Update {name} with ESPHome {version} or later"
|
||||
},
|
||||
"device_conflict": {
|
||||
|
||||
@@ -102,7 +102,6 @@ SENSORS: tuple[EssentSensorEntityDescription, ...] = (
|
||||
key="average_today",
|
||||
translation_key="average_today",
|
||||
value_fn=lambda energy_data: energy_data.avg_price,
|
||||
energy_types=(EnergyType.ELECTRICITY,),
|
||||
),
|
||||
EssentSensorEntityDescription(
|
||||
key="lowest_price_today",
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
"electricity_next_price": {
|
||||
"name": "Next electricity price"
|
||||
},
|
||||
"gas_average_today": {
|
||||
"name": "Average gas price today"
|
||||
},
|
||||
"gas_current_price": {
|
||||
"name": "Current gas price"
|
||||
},
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251127.0"]
|
||||
"requirements": ["home-assistant-frontend==20251126.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_air_quality_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["google_air_quality_api==1.1.3"]
|
||||
"requirements": ["google_air_quality_api==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"heavily_polluted": "Heavily polluted",
|
||||
"heavy_air_pollution": "Heavy air pollution",
|
||||
"high_air_pollution": "High air pollution",
|
||||
"high_air_quality": "High air pollution",
|
||||
"high_health_risk": "High health risk",
|
||||
"horrible_air_quality": "Horrible air quality",
|
||||
"light_air_pollution": "Light air pollution",
|
||||
@@ -164,18 +165,20 @@
|
||||
"slightly_polluted": "Slightly polluted",
|
||||
"sufficient_air_quality": "Sufficient air quality",
|
||||
"unfavorable_air_quality": "Unfavorable air quality",
|
||||
"unfavorable_air_quality_for_sensitive_groups": "Unfavorable air quality for sensitive groups",
|
||||
"unfavorable_sensitive": "Unfavorable air quality for sensitive groups",
|
||||
"unhealthy_air_quality": "Unhealthy air quality",
|
||||
"unhealthy_sensitive": "Unhealthy air quality for sensitive groups",
|
||||
"unsatisfactory_air_quality": "Unsatisfactory air quality",
|
||||
"very_bad_air_quality": "Very bad air quality",
|
||||
"very_good_air_quality": "Very good air quality",
|
||||
"very_high_air_pollution": "Very high air pollution",
|
||||
"very_high_air_quality": "Very High air pollution",
|
||||
"very_high_health_risk": "Very high health risk",
|
||||
"very_low_air_pollution": "Very low air pollution",
|
||||
"very_polluted": "Very polluted",
|
||||
"very_poor_air_quality": "Very poor air quality",
|
||||
"very_unfavorable_air_quality": "Very unfavorable air quality",
|
||||
"very_unhealthy": "Very unhealthy air quality",
|
||||
"very_unhealthy_air_quality": "Very unhealthy air quality",
|
||||
"warning_air_pollution": "Warning level air pollution"
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
def async_update_group_state(self) -> None:
|
||||
"""Query all members and determine the sensor group state."""
|
||||
self.calculate_state_attributes(self._get_valid_entities())
|
||||
states: list[str] = []
|
||||
states: list[str | None] = []
|
||||
valid_units = self._valid_units
|
||||
valid_states: list[bool] = []
|
||||
sensor_values: list[tuple[str, float, State]] = []
|
||||
@@ -435,9 +435,12 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
state.attributes.get("unit_of_measurement"),
|
||||
self.entity_id,
|
||||
)
|
||||
else:
|
||||
states.append(None)
|
||||
valid_states.append(False)
|
||||
|
||||
# Set group as unavailable if all members do not have numeric values
|
||||
self._attr_available = any(numeric_state for numeric_state in valid_states)
|
||||
# Set group as unavailable if all members are unavailable or missing
|
||||
self._attr_available = not all(s in (STATE_UNAVAILABLE, None) for s in states)
|
||||
|
||||
valid_state = self.mode(
|
||||
state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
|
||||
@@ -446,6 +449,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
|
||||
if not valid_state or not valid_state_numeric:
|
||||
self._attr_native_value = None
|
||||
self._extra_state_attribute = {}
|
||||
return
|
||||
|
||||
# Calculate values
|
||||
|
||||
@@ -211,7 +211,7 @@ async def ws_start_preview(
|
||||
|
||||
@callback
|
||||
def async_preview_updated(
|
||||
last_exception: BaseException | None, state: str, attributes: Mapping[str, Any]
|
||||
last_exception: Exception | None, state: str, attributes: Mapping[str, Any]
|
||||
) -> None:
|
||||
"""Forward config entry state events to websocket."""
|
||||
if last_exception:
|
||||
|
||||
@@ -241,9 +241,7 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
|
||||
|
||||
async def async_start_preview(
|
||||
self,
|
||||
preview_callback: Callable[
|
||||
[BaseException | None, str, Mapping[str, Any]], None
|
||||
],
|
||||
preview_callback: Callable[[Exception | None, str, Mapping[str, Any]], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Render a preview."""
|
||||
|
||||
|
||||
@@ -181,16 +181,6 @@ class LoggerSettings:
|
||||
"""Save settings."""
|
||||
self._store.async_delay_save(self._async_data_to_save, delay)
|
||||
|
||||
@callback
|
||||
def async_get_integration_domains(self) -> set[str]:
|
||||
"""Get domains that have integration-level log settings."""
|
||||
stored_log_config = self._stored_config[STORAGE_LOG_KEY]
|
||||
return {
|
||||
domain
|
||||
for domain, setting in stored_log_config.items()
|
||||
if setting.type == LogSettingsType.INTEGRATION
|
||||
}
|
||||
|
||||
@callback
|
||||
def _async_get_logger_logs(self) -> dict[str, int]:
|
||||
"""Get the logger logs."""
|
||||
|
||||
@@ -6,7 +6,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api import ActiveConnection
|
||||
from homeassistant.config_entries import DISCOVERY_SOURCES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||
from homeassistant.setup import async_get_loaded_integrations
|
||||
@@ -35,16 +34,6 @@ def handle_integration_log_info(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle integrations logger info."""
|
||||
integrations = set(async_get_loaded_integrations(hass))
|
||||
|
||||
# Add discovered config flows that are not yet loaded
|
||||
for flow in hass.config_entries.flow.async_progress():
|
||||
if flow["context"].get("source") in DISCOVERY_SOURCES:
|
||||
integrations.add(flow["handler"])
|
||||
|
||||
# Add integrations with custom log settings
|
||||
integrations.update(hass.data[DATA_LOGGER].settings.async_get_integration_domains())
|
||||
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
[
|
||||
@@ -54,7 +43,7 @@ def handle_integration_log_info(
|
||||
f"homeassistant.components.{integration}"
|
||||
).getEffectiveLevel(),
|
||||
}
|
||||
for integration in integrations
|
||||
for integration in async_get_loaded_integrations(hass)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -183,16 +183,6 @@ PUMP_CONTROL_MODE_MAP = {
|
||||
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
SETPOINT_CHANGE_SOURCE_MAP = {
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kManual: "manual",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kSchedule: "schedule",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kExternal: "external",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
MATTER_2000_TO_UNIX_EPOCH_OFFSET = (
|
||||
946684800 # Seconds from Matter 2000 epoch to Unix epoch
|
||||
)
|
||||
HUMIDITY_SCALING_FACTOR = 100
|
||||
TEMPERATURE_SCALING_FACTOR = 100
|
||||
|
||||
@@ -1498,54 +1488,4 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.ServiceArea.Attributes.EstimatedEndTime,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SetpointChangeSource",
|
||||
translation_key="setpoint_change_source",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None,
|
||||
options=[x for x in SETPOINT_CHANGE_SOURCE_MAP.values() if x is not None],
|
||||
device_to_ha=lambda x: SETPOINT_CHANGE_SOURCE_MAP[x],
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeSource,),
|
||||
device_type=(device_types.Thermostat,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SetpointChangeSourceTimestamp",
|
||||
translation_key="setpoint_change_timestamp",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
device_to_ha=(
|
||||
lambda x: (
|
||||
dt_util.utc_from_timestamp(x + MATTER_2000_TO_UNIX_EPOCH_OFFSET)
|
||||
if x > 0
|
||||
else None
|
||||
)
|
||||
),
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.SetpointChangeSourceTimestamp,
|
||||
),
|
||||
device_type=(device_types.Thermostat,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ThermostatSetpointChangeAmount",
|
||||
translation_key="setpoint_change_amount",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeAmount,),
|
||||
device_type=(device_types.Thermostat,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -528,20 +528,6 @@
|
||||
"rms_voltage": {
|
||||
"name": "Effective voltage"
|
||||
},
|
||||
"setpoint_change_amount": {
|
||||
"name": "Last change amount"
|
||||
},
|
||||
"setpoint_change_source": {
|
||||
"name": "Last change source",
|
||||
"state": {
|
||||
"external": "External",
|
||||
"manual": "Manual",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
},
|
||||
"setpoint_change_timestamp": {
|
||||
"name": "Last change"
|
||||
},
|
||||
"switch_current_position": {
|
||||
"name": "Current switch position"
|
||||
},
|
||||
|
||||
@@ -1486,7 +1486,6 @@ class MqttEntity(
|
||||
entity_registry.async_update_entity(
|
||||
self.entity_id, new_entity_id=self._update_registry_entity_id
|
||||
)
|
||||
self._update_registry_entity_id = None
|
||||
|
||||
await super().async_added_to_hass()
|
||||
self._subscriptions = {}
|
||||
|
||||
@@ -729,8 +729,8 @@
|
||||
"data_description": {
|
||||
"payload_reset_percentage": "A special payload that resets the fan speed percentage state attribute to unknown when received at the percentage state topic.",
|
||||
"percentage_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the percentage command topic.",
|
||||
"percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage setting. The value shall be in the range from \"speed range min\" to \"speed range max\". [Learn more.]({url}#percentage_command_topic)",
|
||||
"percentage_state_topic": "The MQTT topic subscribed to receive fan speed state. This is a value in the range from \"speed range min\" to \"speed range max\". [Learn more.]({url}#percentage_state_topic)",
|
||||
"percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage. [Learn more.]({url}#percentage_command_topic)",
|
||||
"percentage_state_topic": "The MQTT topic subscribed to receive fan speed based on percentage. [Learn more.]({url}#percentage_state_topic)",
|
||||
"percentage_value_template": "Defines a [template]({value_templating_url}) to extract the speed percentage value.",
|
||||
"speed_range_max": "The maximum of numeric output range (representing 100 %). The percentage step is 100 / number of speeds within the \"speed range\".",
|
||||
"speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The percentage step is 100 / the number of speeds within the \"speed range\"."
|
||||
|
||||
@@ -19,7 +19,6 @@ from google_nest_sdm.exceptions import (
|
||||
ConfigurationException,
|
||||
DecodeException,
|
||||
SubscriberException,
|
||||
SubscriberTimeoutException,
|
||||
)
|
||||
from google_nest_sdm.traits import TraitType
|
||||
import voluptuous as vol
|
||||
@@ -204,16 +203,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
|
||||
await auth.async_get_access_token()
|
||||
except ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="reauth_required"
|
||||
) from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN, translation_key="auth_server_error"
|
||||
) from err
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN, translation_key="auth_client_error"
|
||||
) from err
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
subscriber = await api.new_subscriber(hass, entry, auth)
|
||||
if not subscriber:
|
||||
@@ -234,32 +227,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
|
||||
unsub = await subscriber.start_async()
|
||||
except AuthException as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="reauth_required",
|
||||
f"Subscriber authentication error: {err!s}"
|
||||
) from err
|
||||
except ConfigurationException as err:
|
||||
_LOGGER.error("Configuration error: %s", err)
|
||||
return False
|
||||
except SubscriberTimeoutException as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subscriber_timeout",
|
||||
) from err
|
||||
except SubscriberException as err:
|
||||
_LOGGER.error("Subscriber error: %s", err)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subscriber_error",
|
||||
) from err
|
||||
raise ConfigEntryNotReady(f"Subscriber error: {err!s}") from err
|
||||
|
||||
try:
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
except ApiException as err:
|
||||
unsub()
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_api_error",
|
||||
) from err
|
||||
raise ConfigEntryNotReady(f"Device manager error: {err!s}") from err
|
||||
|
||||
@callback
|
||||
def on_hass_stop(_: Event) -> None:
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"requirements": ["google-nest-sdm==9.1.1"]
|
||||
"requirements": ["google-nest-sdm==9.1.0"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,12 @@ rules:
|
||||
entity-unique-id: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: todo
|
||||
test-before-setup: done
|
||||
test-before-setup:
|
||||
status: todo
|
||||
comment: |
|
||||
The integration does tests on setup, however the most common issues
|
||||
observed are related to ipv6 misconfigurations and the error messages
|
||||
are not self explanatory and can be improved.
|
||||
docs-high-level-description: done
|
||||
config-flow-test-coverage: done
|
||||
docs-actions: done
|
||||
|
||||
@@ -131,26 +131,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_client_error": {
|
||||
"message": "Client error during authentication, please check your network connection."
|
||||
},
|
||||
"auth_server_error": {
|
||||
"message": "Error response from authentication server, please see logs for details."
|
||||
},
|
||||
"device_api_error": {
|
||||
"message": "Error communicating with the Device Access API, please see logs for details."
|
||||
},
|
||||
"reauth_required": {
|
||||
"message": "Reauthentication is required, please follow the instructions in the UI to reauthenticate your account."
|
||||
},
|
||||
"subscriber_error": {
|
||||
"message": "Subscriber failed to connect to Google, please see logs for details."
|
||||
},
|
||||
"subscriber_timeout": {
|
||||
"message": "Subscriber timed out while attempting to connect to Google. Please check your network connection and IPv6 configuration if applicable."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"subscription_name": {
|
||||
"options": {
|
||||
|
||||
@@ -432,7 +432,7 @@ class NumberDeviceClass(StrEnum):
|
||||
|
||||
Unit of measurement: UnitOfVolumeFlowRate
|
||||
- SI / metric: `m³/h`, `m³/min`, `m³/s`, `L/h`, `L/min`, `L/s`, `mL/s`
|
||||
- USCS / imperial: `ft³/min`, `gal/min`, `gal/d`
|
||||
- USCS / imperial: `ft³/min`, `gal/min`
|
||||
"""
|
||||
|
||||
WATER = "water"
|
||||
|
||||
@@ -237,13 +237,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_GPIO_MODE,
|
||||
set_gpio_mode,
|
||||
service_set_gpio_mode_schema,
|
||||
description_placeholders={
|
||||
"gpio_modes_documentation_url": "https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes"
|
||||
},
|
||||
DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema
|
||||
)
|
||||
|
||||
async def set_led_mode(call: ServiceCall) -> None:
|
||||
@@ -254,13 +248,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
await gw_hub.gateway.set_led_mode(led_id, led_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_LED_MODE,
|
||||
set_led_mode,
|
||||
service_set_led_mode_schema,
|
||||
description_placeholders={
|
||||
"led_modes_documentation_url": "https://www.home-assistant.io/integrations/opentherm_gw/#led-modes"
|
||||
},
|
||||
DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema
|
||||
)
|
||||
|
||||
async def set_max_mod(call: ServiceCall) -> None:
|
||||
@@ -306,7 +294,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
SERVICE_SEND_TRANSP_CMD,
|
||||
send_transparent_cmd,
|
||||
service_send_transp_cmd_schema,
|
||||
description_placeholders={
|
||||
"opentherm_gateway_firmware_url": "https://otgw.tclcode.com/firmware.html"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -386,7 +386,7 @@
|
||||
"name": "Reset gateway"
|
||||
},
|
||||
"send_transparent_command": {
|
||||
"description": "Sends custom OTGW commands ({opentherm_gateway_firmware_url}) through a transparent interface.",
|
||||
"description": "Sends custom otgw commands (https://otgw.tclcode.com/firmware.html) through a transparent interface.",
|
||||
"fields": {
|
||||
"gateway_id": {
|
||||
"description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]",
|
||||
@@ -461,7 +461,7 @@
|
||||
"name": "ID"
|
||||
},
|
||||
"mode": {
|
||||
"description": "Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO \"B\". See {gpio_modes_documentation_url} for an explanation of the values.",
|
||||
"description": "Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO \"B\". See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values.",
|
||||
"name": "[%key:common::config_flow::data::mode%]"
|
||||
}
|
||||
},
|
||||
@@ -507,7 +507,7 @@
|
||||
"name": "ID"
|
||||
},
|
||||
"mode": {
|
||||
"description": "The function to assign to the LED. See {led_modes_documentation_url} for an explanation of the values.",
|
||||
"description": "The function to assign to the LED. See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values.",
|
||||
"name": "[%key:common::config_flow::data::mode%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.5.1"]
|
||||
"requirements": ["renault-api==0.5.0"]
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ async def async_setup_entry(
|
||||
(
|
||||
RoborockMap(
|
||||
config_entry,
|
||||
f"{coord.duid_slug}_map_{map_info.name}",
|
||||
coord,
|
||||
coord.properties_api.home,
|
||||
map_info.map_flag,
|
||||
@@ -54,17 +55,13 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
home_trait: HomeTrait,
|
||||
map_flag: int,
|
||||
map_name: str,
|
||||
) -> None:
|
||||
"""Initialize a Roborock map."""
|
||||
map_name = map_name or f"Map {map_flag}"
|
||||
# Note: Map names are not a valid unique id since they can be changed
|
||||
# in the roborock app. This should be migrated to use map flag for
|
||||
# the unique id.
|
||||
unique_id = f"{coordinator.duid_slug}_map_{map_name}"
|
||||
RoborockCoordinatedEntityV1.__init__(self, unique_id, coordinator)
|
||||
ImageEntity.__init__(self, coordinator.hass)
|
||||
self.config_entry = config_entry
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==3.8.4",
|
||||
"python-roborock==3.8.1",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ class SensorDeviceClass(StrEnum):
|
||||
|
||||
Unit of measurement: UnitOfVolumeFlowRate
|
||||
- SI / metric: `m³/h`, `m³/min`, `m³/s`, `L/h`, `L/min`, `L/s`, `mL/s`
|
||||
- USCS / imperial: `ft³/min`, `gal/min`, `gal/d`
|
||||
- USCS / imperial: `ft³/min`, `gal/min`
|
||||
"""
|
||||
|
||||
WATER = "water"
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/senz",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiosenz"],
|
||||
"requirements": ["aiosenz==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .entity import SFRCoordinatorEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SFRBoxBinarySensorEntityDescription[_T](BinarySensorEntityDescription):
|
||||
@@ -97,4 +94,6 @@ class SFRBoxBinarySensor[_T](SFRCoordinatorEntity[_T], BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the native value of the device."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -24,10 +24,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .entity import SFREntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
# but better to queue action calls to avoid conflicts
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def with_error_wrapping[**_P, _R](
|
||||
func: Callable[Concatenate[SFRBoxButton, _P], Awaitable[_R]],
|
||||
|
||||
@@ -39,10 +39,7 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
_box: SFRBox
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize SFR Box flow."""
|
||||
self._config: dict[str, Any] = {}
|
||||
_config: dict[str, Any] = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
@@ -50,7 +47,6 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
box = SFRBox(
|
||||
ip=user_input[CONF_HOST], client=async_get_clientsession(self.hass)
|
||||
)
|
||||
@@ -64,6 +60,7 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
assert system_info is not None
|
||||
await self.async_set_unique_id(system_info.mac_addr)
|
||||
self._abort_if_unique_id_configured()
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
self._box = box
|
||||
self._config.update(user_input)
|
||||
return await self.async_step_choose_auth()
|
||||
|
||||
@@ -33,7 +33,7 @@ class SFRRuntimeData:
|
||||
wan: SFRDataUpdateCoordinator[WanInfo]
|
||||
|
||||
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]):
|
||||
"""Coordinator to manage data updates."""
|
||||
|
||||
config_entry: SFRConfigEntry
|
||||
@@ -57,11 +57,9 @@ class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
update_interval=_SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> _DataT:
|
||||
async def _async_update_data(self) -> _DataT | None:
|
||||
"""Update data."""
|
||||
try:
|
||||
if data := await self._method(self.box):
|
||||
return data
|
||||
return await self._method(self.box)
|
||||
except SFRBoxError as err:
|
||||
raise UpdateFailed from err
|
||||
raise UpdateFailed("No data received from SFR Box")
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sfr_box",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["sfrbox-api==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
rules:
|
||||
## Bronze
|
||||
config-flow: done
|
||||
test-before-configure: done
|
||||
unique-config-entry: done
|
||||
config-flow-test-coverage: done
|
||||
runtime-data: done
|
||||
test-before-setup: done
|
||||
appropriate-polling: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: local_polling without events
|
||||
dependency-transparency: done
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: There are no service actions
|
||||
common-modules: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: There are no service actions
|
||||
brands: done
|
||||
|
||||
## Silver
|
||||
config-entry-unloading: done
|
||||
log-when-unavailable: done
|
||||
entity-unavailable: done
|
||||
action-exceptions: done
|
||||
reauthentication-flow: done
|
||||
parallel-updates: done
|
||||
test-coverage: done
|
||||
integration-owner: done
|
||||
docs-installation-parameters:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options flow
|
||||
|
||||
## Gold
|
||||
entity-translations: done
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: |
|
||||
What does DSL counter count?
|
||||
What is the state of CRC?
|
||||
line_status and training and net_infra and mode -> unknown shouldn't be an option and the entity should return None instead
|
||||
devices:
|
||||
status: todo
|
||||
comment: MAC address can be set to the connections
|
||||
entity-category: done
|
||||
entity-disabled-by-default: done
|
||||
discovery:
|
||||
status: todo
|
||||
comment: Should be possible
|
||||
stale-devices: done
|
||||
diagnostics: done
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
comment: Need to be able to manually change the IP address
|
||||
dynamic-devices: done
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: Discovery is not yet implemented
|
||||
repair-issues: done
|
||||
docs-use-cases:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-data-update:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
docs-known-limitations:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
docs-troubleshooting:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
docs-examples:
|
||||
status: todo
|
||||
comment: not yet documented
|
||||
|
||||
## Platinum
|
||||
async-dependency:
|
||||
status: done
|
||||
comment: sfrbox-api is asynchronous
|
||||
inject-websession:
|
||||
status: done
|
||||
comment: sfrbox-api uses injected aiohttp websession
|
||||
strict-typing:
|
||||
status: done
|
||||
comment: sfrbox-api is fully typed, and integration uses strict typing
|
||||
@@ -26,9 +26,6 @@ from homeassistant.helpers.typing import StateType
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .entity import SFRCoordinatorEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SFRBoxSensorEntityDescription[_T](SensorEntityDescription):
|
||||
@@ -253,4 +250,6 @@ class SFRBoxSensor[_T](SFRCoordinatorEntity[_T], SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the device."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -30,7 +30,7 @@ from .entity import (
|
||||
ShellyRpcAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
ShellySleepingRpcAttributeEntity,
|
||||
async_setup_entry_block,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rest,
|
||||
async_setup_entry_rpc,
|
||||
)
|
||||
@@ -127,7 +127,7 @@ class RpcBluTrvBinarySensor(RpcBinarySensor):
|
||||
)
|
||||
|
||||
|
||||
BLOCK_SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
|
||||
SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
|
||||
("device", "overtemp"): BlockBinarySensorDescription(
|
||||
key="device|overtemp",
|
||||
translation_key="overheating",
|
||||
@@ -372,19 +372,19 @@ def _async_setup_block_entry(
|
||||
) -> None:
|
||||
"""Set up entities for BLOCK device."""
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
BLOCK_SENSORS,
|
||||
SENSORS,
|
||||
BlockSleepingBinarySensor,
|
||||
)
|
||||
else:
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
BLOCK_SENSORS,
|
||||
SENSORS,
|
||||
BlockBinarySensor,
|
||||
)
|
||||
async_setup_entry_rest(
|
||||
|
||||
@@ -168,7 +168,6 @@ INPUTS_EVENTS_SUBTYPES: Final = {
|
||||
"button2": 2,
|
||||
"button3": 3,
|
||||
"button4": 4,
|
||||
"button5": 5,
|
||||
}
|
||||
|
||||
SHBTN_MODELS: Final = [MODEL_BUTTON1, MODEL_BUTTON1_V2]
|
||||
|
||||
@@ -79,7 +79,6 @@ from .utils import (
|
||||
get_rpc_device_wakeup_period,
|
||||
get_rpc_ws_url,
|
||||
get_shelly_model_name,
|
||||
is_rpc_ble_scanner_supported,
|
||||
update_device_fw_info,
|
||||
)
|
||||
|
||||
@@ -727,7 +726,6 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
"""Handle device connected."""
|
||||
async with self._connection_lock:
|
||||
if self.connected: # Already connected
|
||||
LOGGER.debug("Device %s already connected", self.name)
|
||||
return
|
||||
self.connected = True
|
||||
try:
|
||||
@@ -745,7 +743,10 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
is updated.
|
||||
"""
|
||||
if not self.sleep_period:
|
||||
if is_rpc_ble_scanner_supported(self.config_entry):
|
||||
if (
|
||||
self.config_entry.runtime_data.rpc_supports_scripts
|
||||
and not self.config_entry.runtime_data.rpc_zigbee_firmware
|
||||
):
|
||||
await self._async_connect_ble_scanner()
|
||||
else:
|
||||
await self._async_setup_outbound_websocket()
|
||||
@@ -775,10 +776,6 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
if await async_ensure_ble_enabled(self.device):
|
||||
# BLE enable required a reboot, don't bother connecting
|
||||
# the scanner since it will be disconnected anyway
|
||||
LOGGER.debug(
|
||||
"Device %s BLE enable required a reboot, skipping scanner connect",
|
||||
self.name,
|
||||
)
|
||||
return
|
||||
assert self.device_id is not None
|
||||
self._disconnected_callbacks.append(
|
||||
@@ -847,14 +844,21 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
"""Shutdown the coordinator."""
|
||||
if self.device.connected:
|
||||
try:
|
||||
if not self.sleep_period and is_rpc_ble_scanner_supported(
|
||||
self.config_entry
|
||||
):
|
||||
if not self.sleep_period:
|
||||
await async_stop_scanner(self.device)
|
||||
await super().shutdown()
|
||||
except InvalidAuthError:
|
||||
self.config_entry.async_start_reauth(self.hass)
|
||||
return
|
||||
except RpcCallError as err:
|
||||
# Ignore 404 (No handler for) error
|
||||
if err.code != 404:
|
||||
LOGGER.debug(
|
||||
"Error during shutdown for device %s: %s",
|
||||
self.name,
|
||||
err.message,
|
||||
)
|
||||
return
|
||||
except DeviceConnectionError as err:
|
||||
# If the device is restarting or has gone offline before
|
||||
# the ping/pong timeout happens, the shutdown command
|
||||
|
||||
@@ -27,7 +27,7 @@ from .entity import (
|
||||
RpcEntityDescription,
|
||||
ShellyBlockAttributeEntity,
|
||||
ShellyRpcAttributeEntity,
|
||||
async_setup_entry_block,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rpc,
|
||||
rpc_call,
|
||||
)
|
||||
@@ -81,7 +81,7 @@ def _async_setup_block_entry(
|
||||
coordinator = config_entry.runtime_data.block
|
||||
assert coordinator
|
||||
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, BLOCK_COVERS, BlockShellyCover
|
||||
)
|
||||
|
||||
|
||||
@@ -34,14 +34,14 @@ from .utils import (
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_entry_block(
|
||||
def async_setup_entry_attribute_entities(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ShellyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
sensors: Mapping[tuple[str, str], BlockEntityDescription],
|
||||
sensor_class: Callable,
|
||||
) -> None:
|
||||
"""Set up block entities."""
|
||||
"""Set up entities for attributes."""
|
||||
coordinator = config_entry.runtime_data.block
|
||||
assert coordinator
|
||||
if coordinator.device.initialized:
|
||||
@@ -150,7 +150,7 @@ def async_setup_entry_rpc(
|
||||
sensors: Mapping[str, RpcEntityDescription],
|
||||
sensor_class: Callable,
|
||||
) -> None:
|
||||
"""Set up RPC entities."""
|
||||
"""Set up entities for RPC sensors."""
|
||||
coordinator = config_entry.runtime_data.rpc
|
||||
assert coordinator
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.components.event import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
BASIC_INPUTS_EVENTS_TYPES,
|
||||
@@ -25,11 +26,12 @@ from .const import (
|
||||
SHIX3_1_INPUTS_EVENTS_TYPES,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
||||
from .entity import ShellyBlockEntity, get_entity_rpc_device_info
|
||||
from .utils import (
|
||||
async_remove_orphaned_entities,
|
||||
async_remove_shelly_entity,
|
||||
get_block_channel,
|
||||
get_block_channel_name,
|
||||
get_block_custom_name,
|
||||
get_block_number_of_channels,
|
||||
get_device_entry_gen,
|
||||
@@ -135,7 +137,7 @@ def _async_setup_rpc_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entities for RPC device."""
|
||||
entities: list[ShellyRpcEvent | ShellyRpcScriptEvent] = []
|
||||
entities: list[ShellyRpcEvent] = []
|
||||
|
||||
coordinator = config_entry.runtime_data.rpc
|
||||
if TYPE_CHECKING:
|
||||
@@ -161,9 +163,7 @@ def _async_setup_rpc_entry(
|
||||
continue
|
||||
|
||||
if script_events and (event_types := script_events[get_rpc_key_id(script)]):
|
||||
entities.append(
|
||||
ShellyRpcScriptEvent(coordinator, script, SCRIPT_EVENT, event_types)
|
||||
)
|
||||
entities.append(ShellyRpcScriptEvent(coordinator, script, event_types))
|
||||
|
||||
# If a script is removed, from the device configuration, we need to remove orphaned entities
|
||||
async_remove_orphaned_entities(
|
||||
@@ -211,7 +211,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity):
|
||||
else ""
|
||||
}
|
||||
else:
|
||||
self._attr_name = get_block_custom_name(coordinator.device, block)
|
||||
self._attr_name = get_block_channel_name(coordinator.device, block)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
@@ -228,7 +228,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ShellyRpcEvent(ShellyRpcEntity, EventEntity):
|
||||
class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
|
||||
"""Represent RPC event entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
@@ -241,19 +241,25 @@ class ShellyRpcEvent(ShellyRpcEntity, EventEntity):
|
||||
description: ShellyRpcEventDescription,
|
||||
) -> None:
|
||||
"""Initialize Shelly entity."""
|
||||
super().__init__(coordinator, key)
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = get_entity_rpc_device_info(coordinator, key)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{key}"
|
||||
self.entity_description = description
|
||||
|
||||
_, component, component_id = get_rpc_key(key)
|
||||
if custom_name := get_rpc_custom_name(coordinator.device, key):
|
||||
self._attr_name = custom_name
|
||||
else:
|
||||
self._attr_translation_placeholders = {
|
||||
"input_number": component_id
|
||||
if get_rpc_number_of_channels(coordinator.device, component) > 1
|
||||
else ""
|
||||
}
|
||||
self.event_id = int(component_id)
|
||||
if description.key == "input":
|
||||
_, component, component_id = get_rpc_key(key)
|
||||
if custom_name := get_rpc_custom_name(coordinator.device, key):
|
||||
self._attr_name = custom_name
|
||||
else:
|
||||
self._attr_translation_placeholders = {
|
||||
"input_number": component_id
|
||||
if get_rpc_number_of_channels(coordinator.device, component) > 1
|
||||
else ""
|
||||
}
|
||||
self.event_id = int(component_id)
|
||||
elif description.key == "script":
|
||||
self._attr_name = get_rpc_custom_name(coordinator.device, key)
|
||||
self.event_id = get_rpc_key_id(key)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
@@ -265,36 +271,30 @@ class ShellyRpcEvent(ShellyRpcEntity, EventEntity):
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self, event: dict[str, Any]) -> None:
|
||||
"""Handle the event."""
|
||||
"""Handle the demo button event."""
|
||||
if event["id"] == self.event_id:
|
||||
self._trigger_event(event["event"])
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ShellyRpcScriptEvent(ShellyRpcEntity, EventEntity):
|
||||
class ShellyRpcScriptEvent(ShellyRpcEvent):
|
||||
"""Represent RPC script event entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: ShellyRpcEventDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
description: ShellyRpcEventDescription,
|
||||
event_types: list[str],
|
||||
) -> None:
|
||||
"""Initialize Shelly script event entity."""
|
||||
super().__init__(coordinator, key)
|
||||
self.entity_description = description
|
||||
self._attr_event_types = event_types
|
||||
super().__init__(coordinator, key, SCRIPT_EVENT)
|
||||
|
||||
self._attr_name = get_rpc_custom_name(coordinator.device, key)
|
||||
self.event_id = get_rpc_key_id(key)
|
||||
self.component = key
|
||||
self._attr_event_types = event_types
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await super(CoordinatorEntity, self).async_added_to_hass()
|
||||
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_subscribe_events(self._async_handle_event)
|
||||
@@ -303,7 +303,7 @@ class ShellyRpcScriptEvent(ShellyRpcEntity, EventEntity):
|
||||
@callback
|
||||
def _async_handle_event(self, event: dict[str, Any]) -> None:
|
||||
"""Handle script event."""
|
||||
if event.get("component") == self.key:
|
||||
if event.get("component") == self.component:
|
||||
event_type = event.get("event")
|
||||
if event_type not in self.event_types:
|
||||
# This can happen if we didn't find this event type in the script
|
||||
|
||||
@@ -44,7 +44,7 @@ from .entity import (
|
||||
RpcEntityDescription,
|
||||
ShellyBlockAttributeEntity,
|
||||
ShellyRpcAttributeEntity,
|
||||
async_setup_entry_block,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rpc,
|
||||
)
|
||||
from .utils import (
|
||||
@@ -101,7 +101,7 @@ def _async_setup_block_entry(
|
||||
coordinator = config_entry.runtime_data.block
|
||||
assert coordinator
|
||||
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, BLOCK_LIGHTS, BlockShellyLight
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ from .entity import (
|
||||
RpcEntityDescription,
|
||||
ShellyRpcAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_entry_block,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rpc,
|
||||
rpc_call,
|
||||
)
|
||||
@@ -353,7 +353,7 @@ def _async_setup_block_entry(
|
||||
) -> None:
|
||||
"""Set up entities for BLOCK device."""
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
|
||||
@@ -53,7 +53,7 @@ from .entity import (
|
||||
ShellyRpcAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
ShellySleepingRpcAttributeEntity,
|
||||
async_setup_entry_block,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rest,
|
||||
async_setup_entry_rpc,
|
||||
get_entity_rpc_device_info,
|
||||
@@ -198,7 +198,7 @@ class RpcBluTrvSensor(RpcSensor):
|
||||
)
|
||||
|
||||
|
||||
BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
|
||||
SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
|
||||
("device", "battery"): BlockSensorDescription(
|
||||
key="device|battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
@@ -1736,19 +1736,19 @@ def _async_setup_block_entry(
|
||||
) -> None:
|
||||
"""Set up entities for BLOCK device."""
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
BLOCK_SENSORS,
|
||||
SENSORS,
|
||||
BlockSleepingSensor,
|
||||
)
|
||||
else:
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
BLOCK_SENSORS,
|
||||
SENSORS,
|
||||
BlockSensor,
|
||||
)
|
||||
async_setup_entry_rest(
|
||||
|
||||
@@ -557,9 +557,6 @@
|
||||
"child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
"cury_away_mode": {
|
||||
"name": "Away mode"
|
||||
},
|
||||
"frost_protection": {
|
||||
"name": "[%key:component::shelly::entity::climate::thermostat::state_attributes::preset_mode::state::frost_protection%]"
|
||||
},
|
||||
@@ -592,11 +589,6 @@
|
||||
"beta_firmware": {
|
||||
"name": "Beta firmware"
|
||||
}
|
||||
},
|
||||
"valve": {
|
||||
"gas_valve": {
|
||||
"name": "Valve"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -36,7 +36,7 @@ from .entity import (
|
||||
ShellyBlockAttributeEntity,
|
||||
ShellyRpcAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_entry_block,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rpc,
|
||||
rpc_call,
|
||||
)
|
||||
@@ -306,6 +306,7 @@ RPC_SWITCHES = {
|
||||
"cury_away_mode": RpcSwitchDescription(
|
||||
key="cury",
|
||||
sub_key="away_mode",
|
||||
name="Away mode",
|
||||
translation_key="cury_away_mode",
|
||||
is_on=lambda status: status["away_mode"],
|
||||
method_on="cury_set_away_mode",
|
||||
@@ -337,11 +338,11 @@ def _async_setup_block_entry(
|
||||
coordinator = config_entry.runtime_data.block
|
||||
assert coordinator
|
||||
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, BLOCK_RELAY_SWITCHES, BlockRelaySwitch
|
||||
)
|
||||
|
||||
async_setup_entry_block(
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
|
||||
@@ -110,6 +110,8 @@ def get_block_number_of_channels(device: BlockDevice, block: Block) -> int:
|
||||
channels = device.shelly.get("num_emeters")
|
||||
elif block.type in ["relay", "light"]:
|
||||
channels = device.shelly.get("num_outputs")
|
||||
elif block.type in ["roller", "device"]:
|
||||
channels = 1
|
||||
|
||||
return channels or 1
|
||||
|
||||
@@ -132,6 +134,21 @@ def get_block_channel(block: Block | None, base: str = "1") -> str:
|
||||
return chr(int(block.channel) + ord(base))
|
||||
|
||||
|
||||
def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None:
|
||||
"""Get name based on device and channel name."""
|
||||
if (
|
||||
not block
|
||||
or block.type in ("device", "light", "relay", "emeter")
|
||||
or get_block_number_of_channels(device, block) == 1
|
||||
):
|
||||
return None
|
||||
|
||||
if custom_name := get_block_custom_name(device, block):
|
||||
return custom_name
|
||||
|
||||
return f"Channel {get_block_channel(block)}"
|
||||
|
||||
|
||||
def get_block_sub_device_name(device: BlockDevice, block: Block) -> str:
|
||||
"""Get name of block sub-device."""
|
||||
if TYPE_CHECKING:
|
||||
@@ -647,7 +664,10 @@ def async_remove_shelly_rpc_entities(
|
||||
|
||||
def get_virtual_component_ids(config: dict[str, Any], platform: str) -> list[str]:
|
||||
"""Return a list of virtual component IDs for a platform."""
|
||||
component = VIRTUAL_COMPONENTS_MAP[platform]
|
||||
component = VIRTUAL_COMPONENTS_MAP.get(platform)
|
||||
|
||||
if not component:
|
||||
return []
|
||||
|
||||
ids: list[str] = []
|
||||
|
||||
@@ -955,10 +975,10 @@ def async_migrate_rpc_virtual_components_unique_ids(
|
||||
The new unique_id format is: {mac}-{key}-{component}_{role}
|
||||
"""
|
||||
for component in VIRTUAL_COMPONENTS:
|
||||
if (
|
||||
entity_entry.unique_id.endswith(f"-{component!s}")
|
||||
and (key := entity_entry.unique_id.split("-")[-2]) in config
|
||||
):
|
||||
if entity_entry.unique_id.endswith(f"-{component!s}"):
|
||||
key = entity_entry.unique_id.split("-")[-2]
|
||||
if key not in config:
|
||||
continue
|
||||
role = get_rpc_role_by_key(config, key)
|
||||
new_unique_id = f"{entity_entry.unique_id}_{role}"
|
||||
LOGGER.debug(
|
||||
@@ -974,11 +994,3 @@ def async_migrate_rpc_virtual_components_unique_ids(
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_rpc_ble_scanner_supported(entry: ConfigEntry) -> bool:
|
||||
"""Return true if BLE scanner is supported."""
|
||||
return (
|
||||
entry.runtime_data.rpc_supports_scripts
|
||||
and not entry.runtime_data.rpc_zigbee_firmware
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ class RpcValveDescription(RpcEntityDescription, ValveEntityDescription):
|
||||
BLOCK_VALVES: dict[tuple[str, str], BlockValveDescription] = {
|
||||
("valve", "valve"): BlockValveDescription(
|
||||
key="valve|valve",
|
||||
translation_key="gas_valve",
|
||||
name="Valve",
|
||||
available=lambda block: block.valve not in ("failure", "checking"),
|
||||
removal_condition=lambda _, block: block.valve in ("not_connected", "unknown"),
|
||||
models={MODEL_GAS},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/stiebel_eltron",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus", "pystiebeleltron"],
|
||||
"requirements": ["pystiebeleltron==0.2.5"]
|
||||
"requirements": ["pystiebeleltron==0.2.3"]
|
||||
}
|
||||
|
||||
@@ -8,11 +8,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.components.automation import (
|
||||
DOMAIN as AUTOMATION_DOMAIN,
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
|
||||
)
|
||||
from homeassistant.components.labs import async_listen as async_labs_listen
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
@@ -21,7 +16,7 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryError, HomeAssistantError
|
||||
from homeassistant.helpers import discovery, issue_registry as ir
|
||||
from homeassistant.helpers.device import (
|
||||
@@ -95,20 +90,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
|
||||
|
||||
@callback
|
||||
def new_triggers_conditions_listener() -> None:
|
||||
"""Handle new_triggers_conditions flag change."""
|
||||
hass.async_create_task(
|
||||
_reload_config(ServiceCall(hass, DOMAIN, SERVICE_RELOAD))
|
||||
)
|
||||
|
||||
async_labs_listen(
|
||||
hass,
|
||||
AUTOMATION_DOMAIN,
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
|
||||
new_triggers_conditions_listener,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
@@ -169,27 +168,11 @@ def async_create_preview_binary_sensor(
|
||||
)
|
||||
|
||||
|
||||
class AbstractTemplateBinarySensor(
|
||||
AbstractTemplateEntity, BinarySensorEntity, RestoreEntity
|
||||
):
|
||||
"""Representation of a template binary sensor features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
||||
"""Initialize the features."""
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._template: template.Template = config[CONF_STATE]
|
||||
self._delay_cancel: CALLBACK_TYPE | None = None
|
||||
|
||||
|
||||
class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
|
||||
class StateBinarySensorEntity(TemplateEntity, BinarySensorEntity, RestoreEntity):
|
||||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -199,19 +182,19 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
|
||||
) -> None:
|
||||
"""Initialize the Template binary sensor."""
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateBinarySensor.__init__(self, config)
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._template: template.Template = config[CONF_STATE]
|
||||
self._delay_cancel = None
|
||||
self._delay_on = None
|
||||
self._delay_on_template = config.get(CONF_DELAY_ON)
|
||||
self._delay_on_raw = config.get(CONF_DELAY_ON)
|
||||
self._delay_off = None
|
||||
self._delay_off_template = config.get(CONF_DELAY_OFF)
|
||||
self._delay_off_raw = config.get(CONF_DELAY_OFF)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore state."""
|
||||
if (
|
||||
(
|
||||
self._delay_on_template is not None
|
||||
or self._delay_off_template is not None
|
||||
)
|
||||
(self._delay_on_raw is not None or self._delay_off_raw is not None)
|
||||
and (last_state := await self.async_get_last_state()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
):
|
||||
@@ -223,20 +206,20 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
|
||||
"""Set up templates."""
|
||||
self.add_template_attribute("_state", self._template, None, self._update_state)
|
||||
|
||||
if self._delay_on_template is not None:
|
||||
if self._delay_on_raw is not None:
|
||||
try:
|
||||
self._delay_on = cv.positive_time_period(self._delay_on_template)
|
||||
self._delay_on = cv.positive_time_period(self._delay_on_raw)
|
||||
except vol.Invalid:
|
||||
self.add_template_attribute(
|
||||
"_delay_on", self._delay_on_template, cv.positive_time_period
|
||||
"_delay_on", self._delay_on_raw, cv.positive_time_period
|
||||
)
|
||||
|
||||
if self._delay_off_template is not None:
|
||||
if self._delay_off_raw is not None:
|
||||
try:
|
||||
self._delay_off = cv.positive_time_period(self._delay_off_template)
|
||||
self._delay_off = cv.positive_time_period(self._delay_off_raw)
|
||||
except vol.Invalid:
|
||||
self.add_template_attribute(
|
||||
"_delay_off", self._delay_off_template, cv.positive_time_period
|
||||
"_delay_off", self._delay_off_raw, cv.positive_time_period
|
||||
)
|
||||
|
||||
super()._async_setup_templates()
|
||||
@@ -276,10 +259,12 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
|
||||
self._delay_cancel = async_call_later(self.hass, delay, _set_state)
|
||||
|
||||
|
||||
class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
|
||||
class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity):
|
||||
"""Sensor entity based on trigger data."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
domain = BINARY_SENSOR_DOMAIN
|
||||
extra_template_keys = (CONF_STATE,)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -288,8 +273,7 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
|
||||
config: dict,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateBinarySensor.__init__(self, config)
|
||||
super().__init__(hass, coordinator, config)
|
||||
|
||||
for key in (CONF_STATE, CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF):
|
||||
if isinstance(config.get(key), template.Template):
|
||||
@@ -298,6 +282,7 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
|
||||
|
||||
self._last_delay_from: bool | None = None
|
||||
self._last_delay_to: bool | None = None
|
||||
self._delay_cancel: CALLBACK_TYPE | None = None
|
||||
self._auto_off_cancel: CALLBACK_TYPE | None = None
|
||||
self._auto_off_time: datetime | None = None
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ from .const import (
|
||||
CONF_DEFAULT_ENTITY_ID,
|
||||
CONF_PICTURE,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import TemplateEntity
|
||||
@@ -235,8 +234,6 @@ def create_legacy_template_issue(
|
||||
hass: HomeAssistant, config: ConfigType, domain: str
|
||||
) -> None:
|
||||
"""Create a repair for legacy template entities."""
|
||||
if domain not in PLATFORMS:
|
||||
return
|
||||
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
|
||||
@@ -26,12 +26,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_PATH, DEFAULT_SSL, DOMAIN
|
||||
@@ -98,19 +93,6 @@ async def async_setup_entry(
|
||||
except (AuthenticationError, UnknownError) as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
protocol: Final = "https" if config_entry.data[CONF_SSL] else "http"
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Transmission",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
sw_version=api.server_version,
|
||||
configuration_url=(
|
||||
f"{protocol}://{config_entry.data[CONF_HOST]}:{config_entry.data[CONF_PORT]}"
|
||||
),
|
||||
)
|
||||
|
||||
coordinator = TransmissionDataUpdateCoordinator(hass, config_entry, api)
|
||||
await hass.async_add_executor_job(coordinator.init_torrent_list)
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@ class TransmissionEntity(CoordinatorEntity[TransmissionDataUpdateCoordinator]):
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
manufacturer="Transmission",
|
||||
)
|
||||
|
||||
@@ -1,43 +1,4 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"active_torrents": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"completed_torrents": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"download_speed": {
|
||||
"default": "mdi:cloud-download"
|
||||
},
|
||||
"paused_torrents": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"started_torrents": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"total_torrents": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"transmission_status": {
|
||||
"default": "mdi:information-outline"
|
||||
},
|
||||
"upload_speed": {
|
||||
"default": "mdi:cloud-upload"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"on_off": {
|
||||
"default": "mdi:cloud",
|
||||
"state": {
|
||||
"off": "mdi:cloud-off"
|
||||
}
|
||||
},
|
||||
"turtle_mode": {
|
||||
"default": "mdi:tortoise"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"add_torrent": {
|
||||
"service": "mdi:download"
|
||||
|
||||
@@ -30,12 +30,18 @@ rules:
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Change to mock_setup_entry to avoid repetition when expanding tests.
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
devices:
|
||||
status: todo
|
||||
comment: |
|
||||
Add additional device detail including link to ui.
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
@@ -55,7 +61,10 @@ rules:
|
||||
Speed sensors change so frequently that disabling by default may be appropriate.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
icon-translations:
|
||||
status: todo
|
||||
comment: |
|
||||
Add icons for sensors & switches.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
@@ -29,8 +29,6 @@ from .const import (
|
||||
from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator
|
||||
from .entity import TransmissionEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
MODES: dict[str, list[str] | None] = {
|
||||
"started_torrents": ["downloading"],
|
||||
"completed_torrents": ["seeding"],
|
||||
|
||||
@@ -11,8 +11,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator
|
||||
from .entity import TransmissionEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TransmissionSwitchEntityDescription(SwitchEntityDescription):
|
||||
|
||||
@@ -44,9 +44,10 @@ class HomeAssistantTuyaData(NamedTuple):
|
||||
listener: SharingDeviceListener
|
||||
|
||||
|
||||
def _create_manager(entry: TuyaConfigEntry, token_listener: TokenListener) -> Manager:
|
||||
"""Create a Tuya Manager instance."""
|
||||
return Manager(
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool:
|
||||
"""Async setup hass config entry."""
|
||||
token_listener = TokenListener(hass, entry)
|
||||
manager = Manager(
|
||||
TUYA_CLIENT_ID,
|
||||
entry.data[CONF_USER_CODE],
|
||||
entry.data[CONF_TERMINAL_ID],
|
||||
@@ -55,15 +56,6 @@ def _create_manager(entry: TuyaConfigEntry, token_listener: TokenListener) -> Ma
|
||||
token_listener,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool:
|
||||
"""Async setup hass config entry."""
|
||||
token_listener = TokenListener(hass, entry)
|
||||
|
||||
# Move to executor as it makes blocking call to import_module
|
||||
# with args ('.system', 'urllib3.contrib.resolver')
|
||||
manager = await hass.async_add_executor_job(_create_manager, entry, token_listener)
|
||||
|
||||
listener = DeviceListener(hass, manager)
|
||||
manager.add_device_listener(listener)
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Parsers for RAW (base64-encoded bytes) values."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
from typing import Self
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ElectricityData:
|
||||
"""Electricity RAW value."""
|
||||
|
||||
current: float
|
||||
power: float
|
||||
voltage: float
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, raw: bytes) -> Self | None:
|
||||
"""Parse bytes and return an ElectricityValue object."""
|
||||
# Format:
|
||||
# - legacy: 8 bytes
|
||||
# - v01: [ver=0x01][len=0x0F][data(15 bytes)]
|
||||
# - v02: [ver=0x02][len=0x0F][data(15 bytes)][sign_bitmap(1 byte)]
|
||||
# Data layout (big-endian):
|
||||
# - voltage: 2B, unit 0.1 V
|
||||
# - current: 3B, unit 0.001 A (i.e., mA)
|
||||
# - active power: 3B, unit 0.001 kW (i.e., W)
|
||||
# - reactive power: 3B, unit 0.001 kVar
|
||||
# - apparent power: 3B, unit 0.001 kVA
|
||||
# - power factor: 1B, unit 0.01
|
||||
# Sign bitmap (v02 only, 1 bit means negative):
|
||||
# - bit0 current
|
||||
# - bit1 active power
|
||||
# - bit2 reactive
|
||||
# - bit3 power factor
|
||||
|
||||
is_v1 = len(raw) == 17 and raw[0:2] == b"\x01\x0f"
|
||||
is_v2 = len(raw) == 18 and raw[0:2] == b"\x02\x0f"
|
||||
if is_v1 or is_v2:
|
||||
data = raw[2:17]
|
||||
|
||||
voltage = struct.unpack(">H", data[0:2])[0] / 10.0
|
||||
current = struct.unpack(">L", b"\x00" + data[2:5])[0]
|
||||
power = struct.unpack(">L", b"\x00" + data[5:8])[0]
|
||||
|
||||
if is_v2:
|
||||
sign_bitmap = raw[17]
|
||||
if sign_bitmap & 0x01:
|
||||
current = -current
|
||||
if sign_bitmap & 0x02:
|
||||
power = -power
|
||||
|
||||
return cls(current=current, power=power, voltage=voltage)
|
||||
|
||||
if len(raw) >= 8:
|
||||
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
|
||||
current = struct.unpack(">L", b"\x00" + raw[2:5])[0]
|
||||
power = struct.unpack(">L", b"\x00" + raw[5:8])[0]
|
||||
return cls(current=current, power=power, voltage=voltage)
|
||||
|
||||
return None
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
@@ -48,7 +49,6 @@ from .models import (
|
||||
DPCodeWrapper,
|
||||
EnumTypeData,
|
||||
)
|
||||
from .raw_data_models import ElectricityData
|
||||
|
||||
|
||||
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
|
||||
@@ -120,52 +120,42 @@ class _JsonElectricityVoltageWrapper(DPCodeJsonWrapper):
|
||||
return raw_value.get("voltage")
|
||||
|
||||
|
||||
class _RawElectricityDataWrapper(DPCodeBase64Wrapper):
|
||||
"""Custom DPCode Wrapper for extracting ElectricityData from base64."""
|
||||
|
||||
def _convert(self, value: ElectricityData) -> float:
|
||||
"""Extract specific value from T."""
|
||||
raise NotImplementedError
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := super().read_bytes(device)) is None or (
|
||||
value := ElectricityData.from_bytes(raw_value)
|
||||
) is None:
|
||||
return None
|
||||
return self._convert(value)
|
||||
|
||||
|
||||
class _RawElectricityCurrentWrapper(_RawElectricityDataWrapper):
|
||||
class _RawElectricityCurrentWrapper(DPCodeBase64Wrapper):
|
||||
"""Custom DPCode Wrapper for extracting electricity current from base64."""
|
||||
|
||||
native_unit = UnitOfElectricCurrent.MILLIAMPERE
|
||||
suggested_unit = UnitOfElectricCurrent.AMPERE
|
||||
|
||||
def _convert(self, value: ElectricityData) -> float:
|
||||
"""Extract specific value from ElectricityData."""
|
||||
return value.current
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := super().read_bytes(device)) is None:
|
||||
return None
|
||||
return struct.unpack(">L", b"\x00" + raw_value[2:5])[0]
|
||||
|
||||
|
||||
class _RawElectricityPowerWrapper(_RawElectricityDataWrapper):
|
||||
class _RawElectricityPowerWrapper(DPCodeBase64Wrapper):
|
||||
"""Custom DPCode Wrapper for extracting electricity power from base64."""
|
||||
|
||||
native_unit = UnitOfPower.WATT
|
||||
suggested_unit = UnitOfPower.KILO_WATT
|
||||
|
||||
def _convert(self, value: ElectricityData) -> float:
|
||||
"""Extract specific value from ElectricityData."""
|
||||
return value.power
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := super().read_bytes(device)) is None:
|
||||
return None
|
||||
return struct.unpack(">L", b"\x00" + raw_value[5:8])[0]
|
||||
|
||||
|
||||
class _RawElectricityVoltageWrapper(_RawElectricityDataWrapper):
|
||||
class _RawElectricityVoltageWrapper(DPCodeBase64Wrapper):
|
||||
"""Custom DPCode Wrapper for extracting electricity voltage from base64."""
|
||||
|
||||
native_unit = UnitOfElectricPotential.VOLT
|
||||
|
||||
def _convert(self, value: ElectricityData) -> float:
|
||||
"""Extract specific value from ElectricityData."""
|
||||
return value.voltage
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := super().read_bytes(device)) is None:
|
||||
return None
|
||||
return struct.unpack(">H", raw_value[0:2])[0] / 10.0
|
||||
|
||||
|
||||
CURRENT_WRAPPER = (_RawElectricityCurrentWrapper, _JsonElectricityCurrentWrapper)
|
||||
|
||||
@@ -35,29 +35,11 @@ if TYPE_CHECKING:
|
||||
from .hub import UnifiHub
|
||||
|
||||
|
||||
def convert_brightness_to_unifi(ha_brightness: int) -> int:
|
||||
"""Convert Home Assistant brightness (0-255) to UniFi brightness (0-100)."""
|
||||
return round((ha_brightness / 255) * 100)
|
||||
|
||||
|
||||
def convert_brightness_to_ha(
|
||||
unifi_brightness: int,
|
||||
) -> int:
|
||||
"""Convert UniFi brightness (0-100) to Home Assistant brightness (0-255)."""
|
||||
return round((unifi_brightness / 100) * 255)
|
||||
|
||||
|
||||
def get_device_brightness_or_default(device: Device) -> int:
|
||||
"""Get device's current LED brightness. Defaults to 100 (full brightness) if not set."""
|
||||
value = device.led_override_color_brightness
|
||||
return value if value is not None else 100
|
||||
|
||||
|
||||
@callback
|
||||
def async_device_led_supported_fn(hub: UnifiHub, obj_id: str) -> bool:
|
||||
"""Check if device supports LED control."""
|
||||
device: Device = hub.api.devices[obj_id]
|
||||
return device.led_override is not None or device.supports_led_ring
|
||||
return device.supports_led_ring
|
||||
|
||||
|
||||
@callback
|
||||
@@ -74,24 +56,17 @@ async def async_device_led_control_fn(
|
||||
|
||||
status = "on" if turn_on else "off"
|
||||
|
||||
# Only send brightness and RGB if device has LED_RING hardware support
|
||||
if device.supports_led_ring:
|
||||
# Use provided brightness or fall back to device's current brightness
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = convert_brightness_to_unifi(kwargs[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
brightness = get_device_brightness_or_default(device)
|
||||
brightness = (
|
||||
int((kwargs[ATTR_BRIGHTNESS] / 255) * 100)
|
||||
if ATTR_BRIGHTNESS in kwargs
|
||||
else device.led_override_color_brightness
|
||||
)
|
||||
|
||||
# Use provided RGB color or fall back to device's current color
|
||||
color: str | None
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
rgb = kwargs[ATTR_RGB_COLOR]
|
||||
color = f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
|
||||
else:
|
||||
color = device.led_override_color
|
||||
else:
|
||||
brightness = None
|
||||
color = None
|
||||
color = (
|
||||
f"#{kwargs[ATTR_RGB_COLOR][0]:02x}{kwargs[ATTR_RGB_COLOR][1]:02x}{kwargs[ATTR_RGB_COLOR][2]:02x}"
|
||||
if ATTR_RGB_COLOR in kwargs
|
||||
else device.led_override_color
|
||||
)
|
||||
|
||||
await hub.api.request(
|
||||
DeviceSetLedStatus.create(
|
||||
@@ -152,19 +127,12 @@ class UnifiLightEntity[HandlerT: APIHandler, ApiItemT: ApiItem](
|
||||
|
||||
entity_description: UnifiLightEntityDescription[HandlerT, ApiItemT]
|
||||
_attr_supported_features = LightEntityFeature(0)
|
||||
_attr_color_mode = ColorMode.RGB
|
||||
_attr_supported_color_modes = {ColorMode.RGB}
|
||||
|
||||
@callback
|
||||
def async_initiate_state(self) -> None:
|
||||
"""Initiate entity state."""
|
||||
device = cast(Device, self.entity_description.object_fn(self.api, self._obj_id))
|
||||
|
||||
if device.supports_led_ring:
|
||||
self._attr_supported_color_modes = {ColorMode.RGB}
|
||||
self._attr_color_mode = ColorMode.RGB
|
||||
else:
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
|
||||
self.async_update_state(ItemEvent.ADDED, self._obj_id)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
@@ -182,24 +150,23 @@ class UnifiLightEntity[HandlerT: APIHandler, ApiItemT: ApiItem](
|
||||
"""Update entity state."""
|
||||
description = self.entity_description
|
||||
device_obj = description.object_fn(self.api, self._obj_id)
|
||||
|
||||
device = cast(Device, device_obj)
|
||||
|
||||
self._attr_is_on = description.is_on_fn(self.hub, device_obj)
|
||||
|
||||
# Only set brightness and RGB if device has LED_RING hardware support
|
||||
if device.supports_led_ring:
|
||||
self._attr_brightness = convert_brightness_to_ha(
|
||||
get_device_brightness_or_default(device)
|
||||
)
|
||||
brightness = device.led_override_color_brightness
|
||||
self._attr_brightness = (
|
||||
int((int(brightness) / 100) * 255) if brightness is not None else None
|
||||
)
|
||||
|
||||
# Parse hex color from device and convert to RGB tuple
|
||||
hex_color = (
|
||||
device.led_override_color.lstrip("#")
|
||||
if self._attr_is_on and device.led_override_color
|
||||
else None
|
||||
)
|
||||
if hex_color and len(hex_color) == 6:
|
||||
rgb_list = rgb_hex_to_rgb_list(hex_color)
|
||||
self._attr_rgb_color = (rgb_list[0], rgb_list[1], rgb_list[2])
|
||||
else:
|
||||
self._attr_rgb_color = None
|
||||
hex_color = (
|
||||
device.led_override_color.lstrip("#")
|
||||
if self._attr_is_on and device.led_override_color
|
||||
else None
|
||||
)
|
||||
if hex_color and len(hex_color) == 6:
|
||||
rgb_list = rgb_hex_to_rgb_list(hex_color)
|
||||
self._attr_rgb_color = (rgb_list[0], rgb_list[1], rgb_list[2])
|
||||
else:
|
||||
self._attr_rgb_color = None
|
||||
|
||||
@@ -15,7 +15,7 @@ from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized
|
||||
# diagnostics module will not be imported in the executor.
|
||||
from uiprotect.test_util.anonymize import anonymize_data # noqa: F401
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
@@ -208,7 +208,7 @@ async def async_remove_config_entry_device(
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating configuration from version %s", entry.version)
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ from .entity import (
|
||||
)
|
||||
|
||||
_KEY_DOOR = "door"
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
|
||||
@@ -33,7 +33,6 @@ from .entity import (
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
||||
@@ -32,7 +32,6 @@ from .entity import ProtectDeviceEntity
|
||||
from .utils import get_camera_base_name
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@callback
|
||||
@@ -92,11 +91,7 @@ def _get_camera_channels(
|
||||
|
||||
# no RTSP enabled use first channel with no stream
|
||||
if is_default and not camera.is_third_party_camera:
|
||||
# Only create repair issue if RTSP is not disabled globally
|
||||
if not data.disable_stream:
|
||||
_create_rtsp_repair(hass, entry, data, camera)
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")
|
||||
_create_rtsp_repair(hass, entry, data, camera)
|
||||
yield camera, camera.channels[0], True
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")
|
||||
|
||||
@@ -16,6 +16,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
@@ -54,7 +55,7 @@ from .const import (
|
||||
MIN_REQUIRED_PROTECT_V,
|
||||
OUTDATED_LOG_MESSAGE,
|
||||
)
|
||||
from .data import UFPConfigEntry, async_last_update_was_successful
|
||||
from .data import async_last_update_was_successful
|
||||
from .discovery import async_start_discovery
|
||||
from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
|
||||
|
||||
@@ -79,7 +80,7 @@ def _host_is_direct_connect(host: str) -> bool:
|
||||
|
||||
async def _async_console_is_offline(
|
||||
hass: HomeAssistant,
|
||||
entry: UFPConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
) -> bool:
|
||||
"""Check if a console is offline.
|
||||
|
||||
@@ -223,7 +224,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: UFPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user