mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 17:18:23 +00:00
bump some more
This commit is contained in:
commit
a60c2d840f
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.29.1
|
||||
uses: github/codeql-action/init@v3.29.2
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.29.1
|
||||
uses: github/codeql-action/analyze@v3.29.2
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@ -452,8 +452,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
|
||||
/homeassistant/components/escea/ @lazdavila
|
||||
/tests/components/escea/ @lazdavila
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/event/ @home-assistant/core
|
||||
|
@ -1,15 +1,7 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Uninstall pre-installed formatting and linting tools
|
||||
# They would conflict with our pinned versions
|
||||
RUN \
|
||||
pipx uninstall pydocstyle \
|
||||
&& pipx uninstall pycodestyle \
|
||||
&& pipx uninstall mypy \
|
||||
&& pipx uninstall pylint
|
||||
|
||||
RUN \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
@ -32,21 +24,18 @@ RUN \
|
||||
libxml2 \
|
||||
git \
|
||||
cmake \
|
||||
autoconf \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
&& uv pip install --system -e hass-release/ \
|
||||
&& chown -R vscode /usr/src/hass-release/data
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
RUN uv python install 3.13.2
|
||||
|
||||
USER vscode
|
||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||
@ -55,6 +44,10 @@ 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
|
||||
COPY requirements.txt ./
|
||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||
@ -65,4 +58,4 @@ RUN uv pip install -r requirements_test.txt
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
||||
ENV SHELL=/bin/bash
|
||||
|
@ -2,19 +2,45 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
api = AmazonEchoApi(
|
||||
data[CONF_COUNTRY],
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
data = await api.login_mode_interactive(data[CONF_CODE])
|
||||
finally:
|
||||
await api.close()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
@ -25,17 +51,14 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
client = AmazonEchoApi(
|
||||
user_input[CONF_COUNTRY],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
||||
data = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
@ -44,8 +67,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=user_input | {CONF_LOGIN_DATA: data},
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@ -61,3 +82,43 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth flow."""
|
||||
self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]}
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth confirm."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
entry_data = reauth_entry.data
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data={
|
||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||
CONF_CODE: user_input[CONF_CODE],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]},
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
@ -12,10 +12,10 @@ from aioamazondevices.exceptions import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
@ -55,4 +55,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryError("Could not authenticate") from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.22"]
|
||||
"requirements": ["aioamazondevices==3.2.2"]
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: all tests missing
|
||||
|
@ -22,17 +22,29 @@
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
|
@ -61,6 +61,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -69,6 +71,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_update_options(
|
||||
hass: HomeAssistant, entry: AnthropicConfigEntry
|
||||
) -> None:
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
"""Migrate integration entry structure."""
|
||||
|
||||
@ -138,4 +147,34 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
title=DEFAULT_CONVERSATION_NAME,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 2:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -75,6 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@ -3,7 +3,7 @@
|
||||
from typing import Literal
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
@ -52,13 +52,6 @@ class AnthropicConversationEntity(
|
||||
"""Return a list of supported languages."""
|
||||
return MATCH_ALL
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
self.entry.async_on_unload(
|
||||
self.entry.add_update_listener(self._async_entry_update_listener)
|
||||
)
|
||||
|
||||
async def _async_handle_message(
|
||||
self,
|
||||
user_input: conversation.ConversationInput,
|
||||
@ -89,10 +82,3 @@ class AnthropicConversationEntity(
|
||||
conversation_id=chat_log.conversation_id,
|
||||
continue_conversation=chat_log.continue_conversation,
|
||||
)
|
||||
|
||||
async def _async_entry_update_listener(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Handle options update."""
|
||||
# Reload as we update device info + entity name + supported features
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.104.0"],
|
||||
"requirements": ["hass-nabucasa==0.105.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -6,11 +6,18 @@ from operator import itemgetter
|
||||
import numpy as np
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@ -50,20 +57,23 @@ def datapoints_greater_than_degree(value: dict) -> dict:
|
||||
|
||||
COMPENSATION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Required(CONF_DATAPOINTS): [
|
||||
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
|
||||
],
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=7),
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -7,15 +7,23 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@ -59,24 +67,13 @@ async def async_setup_platform(
|
||||
|
||||
source: str = conf[CONF_SOURCE]
|
||||
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
if not (name := conf.get(CONF_NAME)):
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
CompensationSensor(
|
||||
conf.get(CONF_UNIQUE_ID),
|
||||
name,
|
||||
source,
|
||||
attribute,
|
||||
conf[CONF_PRECISION],
|
||||
conf[CONF_POLYNOMIAL],
|
||||
conf.get(CONF_UNIT_OF_MEASUREMENT),
|
||||
conf[CONF_MINIMUM],
|
||||
conf[CONF_MAXIMUM],
|
||||
)
|
||||
]
|
||||
[CompensationSensor(conf.get(CONF_UNIQUE_ID), name, source, attribute, conf)]
|
||||
)
|
||||
|
||||
|
||||
@ -91,23 +88,27 @@ class CompensationSensor(SensorEntity):
|
||||
name: str,
|
||||
source: str,
|
||||
attribute: str | None,
|
||||
precision: int,
|
||||
polynomial: np.poly1d,
|
||||
unit_of_measurement: str | None,
|
||||
minimum: tuple[float, float] | None,
|
||||
maximum: tuple[float, float] | None,
|
||||
config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize the Compensation sensor."""
|
||||
|
||||
self._attr_name = name
|
||||
self._source_entity_id = source
|
||||
self._precision = precision
|
||||
self._source_attribute = attribute
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
|
||||
self._precision = config[CONF_PRECISION]
|
||||
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
|
||||
self._poly = polynomial
|
||||
self._coefficients = polynomial.coefficients.tolist()
|
||||
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_name = name
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._minimum = config[CONF_MINIMUM]
|
||||
self._maximum = config[CONF_MAXIMUM]
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_state_class = config.get(CONF_STATE_CLASS)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle added to Hass."""
|
||||
@ -137,13 +138,40 @@ class CompensationSensor(SensorEntity):
|
||||
"""Handle sensor state changes."""
|
||||
new_state: State | None
|
||||
if (new_state := event.data["new_state"]) is None:
|
||||
_LOGGER.warning(
|
||||
"While updating compensation %s, the new_state is None", self.name
|
||||
)
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if new_state.state == STATE_UNAVAILABLE:
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_available = True
|
||||
|
||||
if self.native_unit_of_measurement is None and self._source_attribute is None:
|
||||
self._attr_native_unit_of_measurement = new_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
|
||||
if self._attr_device_class is None and (
|
||||
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
):
|
||||
self._attr_device_class = device_class
|
||||
|
||||
if self._attr_state_class is None and (
|
||||
state_class := new_state.attributes.get(ATTR_STATE_CLASS)
|
||||
):
|
||||
self._attr_state_class = state_class
|
||||
|
||||
if self._source_attribute:
|
||||
value = new_state.attributes.get(self._source_attribute)
|
||||
else:
|
||||
|
@ -5,8 +5,9 @@ from pycoolmasternet_async import CoolMasterNet
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_SWING_SUPPORT
|
||||
from .const import CONF_SWING_SUPPORT, DOMAIN
|
||||
from .coordinator import CoolmasterConfigEntry, CoolmasterDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR]
|
||||
@ -48,3 +49,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: CoolmasterConfigEntry) -
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: CoolmasterConfigEntry) -> bool:
|
||||
"""Unload a Coolmaster config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant,
|
||||
config_entry: CoolmasterConfigEntry,
|
||||
device_entry: dr.DeviceEntry,
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
return not device_entry.identifiers.intersection(
|
||||
(DOMAIN, unit_id) for unit_id in config_entry.runtime_data.data
|
||||
)
|
||||
|
@ -19,10 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
SUPPORT_MINIMAL_SERVICES = VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF
|
||||
|
||||
SUPPORT_BASIC_SERVICES = (
|
||||
VacuumEntityFeature.STATE
|
||||
| VacuumEntityFeature.START
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.BATTERY
|
||||
VacuumEntityFeature.STATE | VacuumEntityFeature.START | VacuumEntityFeature.STOP
|
||||
)
|
||||
|
||||
SUPPORT_MOST_SERVICES = (
|
||||
@ -31,7 +28,6 @@ SUPPORT_MOST_SERVICES = (
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.FAN_SPEED
|
||||
)
|
||||
|
||||
@ -46,7 +42,6 @@ SUPPORT_ALL_SERVICES = (
|
||||
| VacuumEntityFeature.SEND_COMMAND
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.STATUS
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.MAP
|
||||
| VacuumEntityFeature.CLEAN_SPOT
|
||||
@ -90,12 +85,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
self._attr_activity = VacuumActivity.DOCKED
|
||||
self._fan_speed = FAN_SPEEDS[1]
|
||||
self._cleaned_area: float = 0
|
||||
self._battery_level = 100
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int:
|
||||
"""Return the current battery level of the vacuum."""
|
||||
return max(0, min(100, self._battery_level))
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str:
|
||||
@ -117,7 +106,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
if self._attr_activity != VacuumActivity.CLEANING:
|
||||
self._attr_activity = VacuumActivity.CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def pause(self) -> None:
|
||||
@ -142,7 +130,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
"""Perform a spot clean-up."""
|
||||
self._attr_activity = VacuumActivity.CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.2.1"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ SENSORS: list[DROPSensorEntityDescription] = [
|
||||
native_unit_of_measurement=UnitOfVolume.GALLONS,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda device: device.drop_api.water_used_today(),
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DROPSensorEntityDescription(
|
||||
key=AVERAGE_WATER_USED,
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["eheimdigital==1.2.0"],
|
||||
"requirements": ["eheimdigital==1.3.0"],
|
||||
"zeroconf": [
|
||||
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
|
||||
]
|
||||
|
@ -363,7 +363,7 @@
|
||||
"discharging": "[%key:common::state::discharging%]",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"charging": "[%key:common::state::charging%]",
|
||||
"full": "Full"
|
||||
"full": "[%key:common::state::full%]"
|
||||
}
|
||||
},
|
||||
"acb_available_energy": {
|
||||
|
@ -281,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
_has_state: bool = False
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "esphome",
|
||||
"name": "ESPHome",
|
||||
"after_dependencies": ["hassio", "zeroconf", "tag"],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz", "@kbx81", "@bdraco"],
|
||||
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["assist_pipeline", "bluetooth", "intent", "ffmpeg", "http"],
|
||||
"dhcp": [
|
||||
|
@ -81,6 +81,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
# if the string is empty
|
||||
if unit_of_measurement := static_info.unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._attr_suggested_display_precision = static_info.accuracy_decimals
|
||||
self._attr_device_class = try_parse_enum(
|
||||
SensorDeviceClass, static_info.device_class
|
||||
)
|
||||
@ -97,7 +98,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
self._attr_state_class = _STATE_CLASSES.from_esphome(state_class)
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | str | None:
|
||||
def native_value(self) -> datetime | int | float | None:
|
||||
"""Return the state of the entity."""
|
||||
if not self._has_state or (state := self._state).missing_state:
|
||||
return None
|
||||
@ -106,7 +107,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
return None
|
||||
if self.device_class is SensorDeviceClass.TIMESTAMP:
|
||||
return dt_util.utc_from_timestamp(state_float)
|
||||
return f"{state_float:.{self._static_info.accuracy_decimals}f}"
|
||||
return state_float
|
||||
|
||||
|
||||
class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity):
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250627.0"]
|
||||
"requirements": ["home-assistant-frontend==20250702.0"]
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["av==13.1.0", "Pillow==11.2.1"]
|
||||
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
|
||||
}
|
||||
|
@ -308,4 +308,50 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
title=DEFAULT_TITLE,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, entry: GoogleGenerativeAIConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate entry."""
|
||||
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 2:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Add TTS subentry which was missing in 2025.7.0b0
|
||||
if not any(
|
||||
subentry.subentry_type == "tts" for subentry in entry.subentries.values()
|
||||
):
|
||||
hass.config_entries.async_add_subentry(
|
||||
entry,
|
||||
ConfigSubentry(
|
||||
data=MappingProxyType(RECOMMENDED_TTS_OPTIONS),
|
||||
subentry_type="tts",
|
||||
title=DEFAULT_TTS_NAME,
|
||||
unique_id=None,
|
||||
),
|
||||
)
|
||||
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -92,6 +92,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Google Generative AI Conversation."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_api(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@ -329,13 +330,14 @@ async def google_generative_ai_config_option_schema(
|
||||
api_models = [api_model async for api_model in api_models_pager]
|
||||
models = [
|
||||
SelectOptionDict(
|
||||
label=api_model.display_name,
|
||||
label=api_model.name.lstrip("models/"),
|
||||
value=api_model.name,
|
||||
)
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
|
||||
for api_model in sorted(
|
||||
api_models, key=lambda x: x.name.lstrip("models/") or ""
|
||||
)
|
||||
if (
|
||||
api_model.display_name
|
||||
and api_model.name
|
||||
api_model.name
|
||||
and ("tts" in api_model.name) == (subentry_type == "tts")
|
||||
and "vision" not in api_model.name
|
||||
and api_model.supported_actions
|
||||
|
@ -11,6 +11,7 @@ from urllib.parse import quote
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import ClientTimeout, ClientWebSocketResponse, hdrs, web
|
||||
from aiohttp.helpers import must_be_empty_body
|
||||
from aiohttp.web_exceptions import HTTPBadGateway, HTTPBadRequest
|
||||
from multidict import CIMultiDict
|
||||
from yarl import URL
|
||||
@ -184,13 +185,16 @@ class HassIOIngress(HomeAssistantView):
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
# Simple request
|
||||
if result.status in (204, 304) or (
|
||||
if (empty_body := must_be_empty_body(result.method, result.status)) or (
|
||||
content_length is not UNDEFINED
|
||||
and (content_length_int := int(content_length))
|
||||
<= MAX_SIMPLE_RESPONSE_SIZE
|
||||
):
|
||||
# Return Response
|
||||
body = await result.read()
|
||||
if empty_body:
|
||||
body = None
|
||||
else:
|
||||
body = await result.read()
|
||||
simple_response = web.Response(
|
||||
headers=headers,
|
||||
status=result.status,
|
||||
|
@ -63,8 +63,8 @@ from .utils import get_device_macs, non_verifying_requests_session
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Huawei LTE config flow."""
|
||||
class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Huawei LTE config flow."""
|
||||
|
||||
VERSION = 3
|
||||
|
||||
@ -75,9 +75,9 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
) -> HuaweiLteOptionsFlow:
|
||||
"""Get options flow."""
|
||||
return OptionsFlowHandler()
|
||||
return HuaweiLteOptionsFlow()
|
||||
|
||||
async def _async_show_user_form(
|
||||
self,
|
||||
@ -354,7 +354,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_update_reload_and_abort(entry, data=new_data)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
class HuaweiLteOptionsFlow(OptionsFlow):
|
||||
"""Huawei LTE options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
|
@ -73,7 +73,6 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
|
||||
schedule = self.mower_attributes.calendar
|
||||
cursor = schedule.timeline.active_after(dt_util.now())
|
||||
program_event = next(cursor, None)
|
||||
_LOGGER.debug("program_event %s", program_event)
|
||||
if not program_event:
|
||||
return None
|
||||
work_area_name = None
|
||||
|
@ -1,7 +1,19 @@
|
||||
"""The constants for the Husqvarna Automower integration."""
|
||||
|
||||
from aioautomower.model import MowerStates
|
||||
|
||||
DOMAIN = "husqvarna_automower"
|
||||
EXECUTION_TIME_DELAY = 5
|
||||
NAME = "Husqvarna Automower"
|
||||
OAUTH2_AUTHORIZE = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/authorize"
|
||||
OAUTH2_TOKEN = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/token"
|
||||
|
||||
ERROR_STATES = [
|
||||
MowerStates.ERROR_AT_POWER_UP,
|
||||
MowerStates.ERROR,
|
||||
MowerStates.FATAL_ERROR,
|
||||
MowerStates.OFF,
|
||||
MowerStates.STOPPED,
|
||||
MowerStates.WAIT_POWER_UP,
|
||||
MowerStates.WAIT_UPDATING,
|
||||
]
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AutomowerConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, ERROR_STATES
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import AutomowerAvailableEntity, handle_sending_exception
|
||||
|
||||
@ -108,18 +108,28 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity):
|
||||
def activity(self) -> LawnMowerActivity:
|
||||
"""Return the state of the mower."""
|
||||
mower_attributes = self.mower_attributes
|
||||
if mower_attributes.mower.state in ERROR_STATES:
|
||||
return LawnMowerActivity.ERROR
|
||||
if mower_attributes.mower.state in PAUSED_STATES:
|
||||
return LawnMowerActivity.PAUSED
|
||||
if (mower_attributes.mower.state == "RESTRICTED") or (
|
||||
mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||
return LawnMowerActivity.RETURNING
|
||||
if (
|
||||
mower_attributes.mower.state is MowerStates.RESTRICTED
|
||||
or mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
||||
):
|
||||
return LawnMowerActivity.DOCKED
|
||||
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
|
||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||
return LawnMowerActivity.RETURNING
|
||||
return LawnMowerActivity.MOWING
|
||||
return LawnMowerActivity.ERROR
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the available attribute of the entity."""
|
||||
return (
|
||||
super().available and self.mower_attributes.mower.state != MowerStates.OFF
|
||||
)
|
||||
|
||||
@property
|
||||
def work_areas(self) -> dict[int, WorkArea] | None:
|
||||
"""Return the work areas of the mower."""
|
||||
|
@ -7,13 +7,7 @@ import logging
|
||||
from operator import attrgetter
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aioautomower.model import (
|
||||
MowerAttributes,
|
||||
MowerModes,
|
||||
MowerStates,
|
||||
RestrictedReasons,
|
||||
WorkArea,
|
||||
)
|
||||
from aioautomower.model import MowerAttributes, MowerModes, RestrictedReasons, WorkArea
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@ -27,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import AutomowerConfigEntry
|
||||
from .const import ERROR_STATES
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import (
|
||||
AutomowerBaseEntity,
|
||||
@ -166,15 +161,6 @@ ERROR_KEYS = [
|
||||
"zone_generator_problem",
|
||||
]
|
||||
|
||||
ERROR_STATES = [
|
||||
MowerStates.ERROR_AT_POWER_UP,
|
||||
MowerStates.ERROR,
|
||||
MowerStates.FATAL_ERROR,
|
||||
MowerStates.OFF,
|
||||
MowerStates.STOPPED,
|
||||
MowerStates.WAIT_POWER_UP,
|
||||
MowerStates.WAIT_UPDATING,
|
||||
]
|
||||
|
||||
ERROR_KEY_LIST = list(
|
||||
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==11.2.1"]
|
||||
"requirements": ["Pillow==11.3.0"]
|
||||
}
|
||||
|
@ -209,6 +209,12 @@
|
||||
"state": {
|
||||
"off": "mdi:card-bulleted-off-outline"
|
||||
}
|
||||
},
|
||||
"boost": {
|
||||
"default": "mdi:thermometer-high",
|
||||
"state": {
|
||||
"off": "mdi:thermometer-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -464,6 +464,16 @@ class IronOSTemperatureNumberEntity(IronOSNumberEntity):
|
||||
else super().native_max_value
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
if (
|
||||
self.entity_description.key is PinecilNumber.BOOST_TEMP
|
||||
and self.native_value == 0
|
||||
):
|
||||
return False
|
||||
return super().available
|
||||
|
||||
|
||||
class IronOSSetpointNumberEntity(IronOSTemperatureNumberEntity):
|
||||
"""IronOS setpoint temperature entity."""
|
||||
|
@ -278,6 +278,9 @@
|
||||
},
|
||||
"calibrate_cjc": {
|
||||
"name": "Calibrate CJC"
|
||||
},
|
||||
"boost": {
|
||||
"name": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pynecil import CharSetting, SettingsDataResponse
|
||||
from pynecil import CharSetting, SettingsDataResponse, TempUnit
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import IronOSConfigEntry
|
||||
from .const import MIN_BOOST_TEMP, MIN_BOOST_TEMP_F
|
||||
from .coordinator import IronOSCoordinators
|
||||
from .entity import IronOSBaseEntity
|
||||
|
||||
@ -39,6 +40,7 @@ class IronOSSwitch(StrEnum):
|
||||
INVERT_BUTTONS = "invert_buttons"
|
||||
DISPLAY_INVERT = "display_invert"
|
||||
CALIBRATE_CJC = "calibrate_cjc"
|
||||
BOOST = "boost"
|
||||
|
||||
|
||||
SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
@ -94,6 +96,13 @@ SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
IronOSSwitchEntityDescription(
|
||||
key=IronOSSwitch.BOOST,
|
||||
translation_key=IronOSSwitch.BOOST,
|
||||
characteristic=CharSetting.BOOST_TEMP,
|
||||
is_on_fn=lambda x: bool(x.get("boost_temp")),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -136,7 +145,15 @@ class IronOSSwitchEntity(IronOSBaseEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
if self.entity_description.key is IronOSSwitch.BOOST:
|
||||
await self.settings.write(
|
||||
self.entity_description.characteristic,
|
||||
MIN_BOOST_TEMP_F
|
||||
if self.settings.data.get("temp_unit") is TempUnit.FAHRENHEIT
|
||||
else MIN_BOOST_TEMP,
|
||||
)
|
||||
else:
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
|
@ -39,7 +39,8 @@ from .const import (
|
||||
KNX_MODULE_KEY,
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SENSOR
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -146,17 +147,17 @@ class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxBinarySensor(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address_state=[
|
||||
config[DOMAIN][CONF_GA_SENSOR][CONF_GA_STATE],
|
||||
*config[DOMAIN][CONF_GA_SENSOR][CONF_GA_PASSIVE],
|
||||
],
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN].get(CONF_INVERT, False),
|
||||
ignore_internal_state=config[DOMAIN].get(CONF_IGNORE_INTERNAL_STATE, False),
|
||||
context_timeout=config[DOMAIN].get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=config[DOMAIN].get(CONF_RESET_AFTER),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SENSOR),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
invert=knx_conf.get(CONF_INVERT, default=False),
|
||||
ignore_internal_state=knx_conf.get(
|
||||
CONF_IGNORE_INTERNAL_STATE, default=False
|
||||
),
|
||||
context_timeout=knx_conf.get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=knx_conf.get(CONF_RESET_AFTER),
|
||||
)
|
||||
self._attr_force_update = self._device.ignore_internal_state
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import Cover as XknxCover
|
||||
@ -35,15 +35,13 @@ from .schema import CoverSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_ANGLE,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_POSITION_SET,
|
||||
CONF_GA_POSITION_STATE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_STEP,
|
||||
CONF_GA_STOP,
|
||||
CONF_GA_UP_DOWN,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -230,38 +228,24 @@ class KnxYamlCover(_KnxCover, KnxYamlEntity):
|
||||
def _create_ui_cover(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxCover:
|
||||
"""Return a KNX Light device to be used within XKNX."""
|
||||
|
||||
def get_address(
|
||||
key: str, address_type: Literal["write", "state"] = CONF_GA_WRITE
|
||||
) -> str | None:
|
||||
"""Get a single group address for given key."""
|
||||
return knx_config[key][address_type] if key in knx_config else None
|
||||
|
||||
def get_addresses(
|
||||
key: str, address_type: Literal["write", "state"] = CONF_GA_STATE
|
||||
) -> list[Any] | None:
|
||||
"""Get group address including passive addresses as list."""
|
||||
return (
|
||||
[knx_config[key][address_type], *knx_config[key][CONF_GA_PASSIVE]]
|
||||
if key in knx_config
|
||||
else None
|
||||
)
|
||||
conf = ConfigExtractor(knx_config)
|
||||
|
||||
return XknxCover(
|
||||
xknx=xknx,
|
||||
name=name,
|
||||
group_address_long=get_addresses(CONF_GA_UP_DOWN, CONF_GA_WRITE),
|
||||
group_address_short=get_addresses(CONF_GA_STEP, CONF_GA_WRITE),
|
||||
group_address_stop=get_addresses(CONF_GA_STOP, CONF_GA_WRITE),
|
||||
group_address_position=get_addresses(CONF_GA_POSITION_SET, CONF_GA_WRITE),
|
||||
group_address_position_state=get_addresses(CONF_GA_POSITION_STATE),
|
||||
group_address_angle=get_address(CONF_GA_ANGLE),
|
||||
group_address_angle_state=get_addresses(CONF_GA_ANGLE),
|
||||
travel_time_down=knx_config[CoverConf.TRAVELLING_TIME_DOWN],
|
||||
travel_time_up=knx_config[CoverConf.TRAVELLING_TIME_UP],
|
||||
invert_updown=knx_config.get(CoverConf.INVERT_UPDOWN, False),
|
||||
invert_position=knx_config.get(CoverConf.INVERT_POSITION, False),
|
||||
invert_angle=knx_config.get(CoverConf.INVERT_ANGLE, False),
|
||||
sync_state=knx_config[CONF_SYNC_STATE],
|
||||
group_address_long=conf.get_write_and_passive(CONF_GA_UP_DOWN),
|
||||
group_address_short=conf.get_write_and_passive(CONF_GA_STEP),
|
||||
group_address_stop=conf.get_write_and_passive(CONF_GA_STOP),
|
||||
group_address_position=conf.get_write_and_passive(CONF_GA_POSITION_SET),
|
||||
group_address_position_state=conf.get_state_and_passive(CONF_GA_POSITION_STATE),
|
||||
group_address_angle=conf.get_write(CONF_GA_ANGLE),
|
||||
group_address_angle_state=conf.get_state_and_passive(CONF_GA_ANGLE),
|
||||
travel_time_down=conf.get(CoverConf.TRAVELLING_TIME_DOWN),
|
||||
travel_time_up=conf.get(CoverConf.TRAVELLING_TIME_UP),
|
||||
invert_updown=conf.get(CoverConf.INVERT_UPDOWN, default=False),
|
||||
invert_position=conf.get(CoverConf.INVERT_POSITION, default=False),
|
||||
invert_angle=conf.get(CoverConf.INVERT_ANGLE, default=False),
|
||||
sync_state=conf.get(CONF_SYNC_STATE),
|
||||
)
|
||||
|
||||
|
||||
|
@ -35,7 +35,6 @@ from .schema import LightSchema
|
||||
from .storage.const import (
|
||||
CONF_COLOR_TEMP_MAX,
|
||||
CONF_COLOR_TEMP_MIN,
|
||||
CONF_DPT,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_BLUE_BRIGHTNESS,
|
||||
CONF_GA_BLUE_SWITCH,
|
||||
@ -45,17 +44,15 @@ from .storage.const import (
|
||||
CONF_GA_GREEN_BRIGHTNESS,
|
||||
CONF_GA_GREEN_SWITCH,
|
||||
CONF_GA_HUE,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_RED_BRIGHTNESS,
|
||||
CONF_GA_RED_SWITCH,
|
||||
CONF_GA_SATURATION,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WHITE_BRIGHTNESS,
|
||||
CONF_GA_WHITE_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.entity_store_schema import LightColorMode
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -203,94 +200,92 @@ def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight:
|
||||
def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight:
|
||||
"""Return a KNX Light device to be used within XKNX."""
|
||||
|
||||
def get_write(key: str) -> str | None:
|
||||
"""Get the write group address."""
|
||||
return knx_config[key][CONF_GA_WRITE] if key in knx_config else None
|
||||
|
||||
def get_state(key: str) -> list[Any] | None:
|
||||
"""Get the state group address."""
|
||||
return (
|
||||
[knx_config[key][CONF_GA_STATE], *knx_config[key][CONF_GA_PASSIVE]]
|
||||
if key in knx_config
|
||||
else None
|
||||
)
|
||||
|
||||
def get_dpt(key: str) -> str | None:
|
||||
"""Get the DPT."""
|
||||
return knx_config[key].get(CONF_DPT) if key in knx_config else None
|
||||
conf = ConfigExtractor(knx_config)
|
||||
|
||||
group_address_tunable_white = None
|
||||
group_address_tunable_white_state = None
|
||||
group_address_color_temp = None
|
||||
group_address_color_temp_state = None
|
||||
|
||||
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
|
||||
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_tunable_white_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if _color_temp_dpt := conf.get_dpt(CONF_GA_COLOR_TEMP):
|
||||
if _color_temp_dpt == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = conf.get_write(CONF_GA_COLOR_TEMP)
|
||||
group_address_tunable_white_state = conf.get_state_and_passive(
|
||||
CONF_GA_COLOR_TEMP
|
||||
)
|
||||
else:
|
||||
# absolute uint or float
|
||||
group_address_color_temp = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_color_temp_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
group_address_color_temp = conf.get_write(CONF_GA_COLOR_TEMP)
|
||||
group_address_color_temp_state = conf.get_state_and_passive(
|
||||
CONF_GA_COLOR_TEMP
|
||||
)
|
||||
if _color_temp_dpt == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
|
||||
|
||||
_color_dpt = get_dpt(CONF_GA_COLOR)
|
||||
color_dpt = conf.get_dpt(CONF_GA_COLOR)
|
||||
|
||||
return XknxLight(
|
||||
xknx,
|
||||
name=name,
|
||||
group_address_switch=get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=get_state(CONF_GA_SWITCH),
|
||||
group_address_brightness=get_write(CONF_GA_BRIGHTNESS),
|
||||
group_address_brightness_state=get_state(CONF_GA_BRIGHTNESS),
|
||||
group_address_color=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
group_address_switch=conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
group_address_brightness=conf.get_write(CONF_GA_BRIGHTNESS),
|
||||
group_address_brightness_state=conf.get_state_and_passive(CONF_GA_BRIGHTNESS),
|
||||
group_address_color=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_color_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
group_address_color_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_rgbw=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_rgbw_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_hue=get_write(CONF_GA_HUE),
|
||||
group_address_hue_state=get_state(CONF_GA_HUE),
|
||||
group_address_saturation=get_write(CONF_GA_SATURATION),
|
||||
group_address_saturation_state=get_state(CONF_GA_SATURATION),
|
||||
group_address_xyy_color=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.XYY
|
||||
group_address_hue=conf.get_write(CONF_GA_HUE),
|
||||
group_address_hue_state=conf.get_state_and_passive(CONF_GA_HUE),
|
||||
group_address_saturation=conf.get_write(CONF_GA_SATURATION),
|
||||
group_address_saturation_state=conf.get_state_and_passive(CONF_GA_SATURATION),
|
||||
group_address_xyy_color=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
else None,
|
||||
group_address_xyy_color_state=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.XYY
|
||||
group_address_xyy_color_state=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
else None,
|
||||
group_address_tunable_white=group_address_tunable_white,
|
||||
group_address_tunable_white_state=group_address_tunable_white_state,
|
||||
group_address_color_temperature=group_address_color_temp,
|
||||
group_address_color_temperature_state=group_address_color_temp_state,
|
||||
group_address_switch_red=get_write(CONF_GA_RED_SWITCH),
|
||||
group_address_switch_red_state=get_state(CONF_GA_RED_SWITCH),
|
||||
group_address_brightness_red=get_write(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_brightness_red_state=get_state(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_switch_green=get_write(CONF_GA_GREEN_SWITCH),
|
||||
group_address_switch_green_state=get_state(CONF_GA_GREEN_SWITCH),
|
||||
group_address_brightness_green=get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green_state=get_state(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_switch_blue=get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=get_state(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_brightness_blue_state=get_state(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_switch_white=get_write(CONF_GA_WHITE_SWITCH),
|
||||
group_address_switch_white_state=get_state(CONF_GA_WHITE_SWITCH),
|
||||
group_address_brightness_white=get_write(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_brightness_white_state=get_state(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_switch_red=conf.get_write(CONF_GA_RED_SWITCH),
|
||||
group_address_switch_red_state=conf.get_state_and_passive(CONF_GA_RED_SWITCH),
|
||||
group_address_brightness_red=conf.get_write(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_brightness_red_state=conf.get_state_and_passive(
|
||||
CONF_GA_RED_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_green=conf.get_write(CONF_GA_GREEN_SWITCH),
|
||||
group_address_switch_green_state=conf.get_state_and_passive(
|
||||
CONF_GA_GREEN_SWITCH
|
||||
),
|
||||
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green_state=conf.get_state_and_passive(
|
||||
CONF_GA_GREEN_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_brightness_blue_state=conf.get_state_and_passive(
|
||||
CONF_GA_BLUE_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_white=conf.get_write(CONF_GA_WHITE_SWITCH),
|
||||
group_address_switch_white_state=conf.get_state_and_passive(
|
||||
CONF_GA_WHITE_SWITCH
|
||||
),
|
||||
group_address_brightness_white=conf.get_write(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_brightness_white_state=conf.get_state_and_passive(
|
||||
CONF_GA_WHITE_BRIGHTNESS
|
||||
),
|
||||
color_temperature_type=color_temperature_type,
|
||||
min_kelvin=knx_config[CONF_COLOR_TEMP_MIN],
|
||||
max_kelvin=knx_config[CONF_COLOR_TEMP_MAX],
|
||||
|
51
homeassistant/components/knx/storage/util.py
Normal file
51
homeassistant/components/knx/storage/util.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Utility functions for the KNX integration."""
|
||||
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE
|
||||
|
||||
|
||||
def nested_get(dic: ConfigType, *keys: str, default: Any | None = None) -> Any:
|
||||
"""Get the value from a nested dictionary."""
|
||||
for key in keys:
|
||||
if key not in dic:
|
||||
return default
|
||||
dic = dic[key]
|
||||
return dic
|
||||
|
||||
|
||||
class ConfigExtractor:
|
||||
"""Helper class for extracting values from a knx config store dictionary."""
|
||||
|
||||
__slots__ = ("get",)
|
||||
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the extractor."""
|
||||
self.get = partial(nested_get, config)
|
||||
|
||||
def get_write(self, *path: str) -> str | None:
|
||||
"""Get the write group address."""
|
||||
return self.get(*path, CONF_GA_WRITE) # type: ignore[no-any-return]
|
||||
|
||||
def get_state(self, *path: str) -> str | None:
|
||||
"""Get the state group address."""
|
||||
return self.get(*path, CONF_GA_STATE) # type: ignore[no-any-return]
|
||||
|
||||
def get_write_and_passive(self, *path: str) -> list[Any | None]:
|
||||
"""Get the group addresses of write and passive."""
|
||||
write = self.get(*path, CONF_GA_WRITE)
|
||||
passive = self.get(*path, CONF_GA_PASSIVE)
|
||||
return [write, *passive] if passive else [write]
|
||||
|
||||
def get_state_and_passive(self, *path: str) -> list[Any | None]:
|
||||
"""Get the group addresses of state and passive."""
|
||||
state = self.get(*path, CONF_GA_STATE)
|
||||
passive = self.get(*path, CONF_GA_PASSIVE)
|
||||
return [state, *passive] if passive else [state]
|
||||
|
||||
def get_dpt(self, *path: str) -> str | None:
|
||||
"""Get the data point type of a group address config key."""
|
||||
return self.get(*path, CONF_DPT) # type: ignore[no-any-return]
|
@ -36,13 +36,8 @@ from .const import (
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .schema import SwitchSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SWITCH
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -142,15 +137,13 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxSwitch(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE],
|
||||
group_address_state=[
|
||||
config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE],
|
||||
*config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE],
|
||||
],
|
||||
respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ],
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN][CONF_INVERT],
|
||||
group_address=knx_conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
invert=knx_conf.get(CONF_INVERT),
|
||||
)
|
||||
|
@ -104,7 +104,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -
|
||||
) as ex:
|
||||
await lcn_connection.async_close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to {config_entry.title}: {ex}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"config_entry_title": config_entry.title,
|
||||
},
|
||||
) from ex
|
||||
|
||||
_LOGGER.info('LCN connected to "%s"', config_entry.title)
|
||||
|
@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
CONF_SWITCHES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@ -100,7 +101,11 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str:
|
||||
return cast(str, domain_data["setpoint"])
|
||||
if domain_name == "scene":
|
||||
return f"{domain_data['register']}{domain_data['scene']}"
|
||||
raise ValueError("Unknown domain")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_domain",
|
||||
translation_placeholders={CONF_DOMAIN: domain_name},
|
||||
)
|
||||
|
||||
|
||||
def generate_unique_id(
|
||||
@ -304,6 +309,8 @@ def get_device_config(
|
||||
def is_states_string(states_string: str) -> list[str]:
|
||||
"""Validate the given states string and return states list."""
|
||||
if len(states_string) != 8:
|
||||
raise ValueError("Invalid length of states string")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_length_of_states_string"
|
||||
)
|
||||
states = {"1": "ON", "0": "OFF", "T": "TOGGLE", "-": "NOCHANGE"}
|
||||
return [states[state_string] for state_string in states_string]
|
||||
|
@ -19,7 +19,7 @@ rules:
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
|
@ -330,8 +330,9 @@ class SendKeys(LcnServiceCall):
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
hit = pypck.lcn_defs.SendKeyCommand.HIT
|
||||
if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit:
|
||||
raise ValueError(
|
||||
"Only hit command is allowed when sending deferred keys."
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_send_keys_action",
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit)
|
||||
@ -368,8 +369,9 @@ class LockKeys(LcnServiceCall):
|
||||
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
if table_id != 0:
|
||||
raise ValueError(
|
||||
"Only table A is allowed when locking keys for a specific time."
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_lock_keys_table",
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.lock_keys_tab_a_temporary(
|
||||
|
@ -414,11 +414,23 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_address": {
|
||||
"message": "LCN device for given address has not been configured."
|
||||
"cannot_connect": {
|
||||
"message": "Unable to connect to {config_entry_title}."
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "LCN device for given device ID has not been configured."
|
||||
"message": "LCN device for given device ID {device_id} has not been configured."
|
||||
},
|
||||
"invalid_domain": {
|
||||
"message": "Invalid domain {domain}."
|
||||
},
|
||||
"invalid_send_keys_action": {
|
||||
"message": "Invalid state for sending keys. Only 'hit' allowed for deferred sending."
|
||||
},
|
||||
"invalid_lock_keys_table": {
|
||||
"message": "Invalid table for locking keys. Only table A allowed when locking for a specific time."
|
||||
},
|
||||
"invalid_length_of_states_string": {
|
||||
"message": "Invalid length of states string. Expected 8 characters."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
@ -35,5 +35,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "led-ble==1.1.7"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
|
||||
}
|
||||
|
@ -780,10 +780,10 @@
|
||||
"battery_level": {
|
||||
"name": "Battery",
|
||||
"state": {
|
||||
"high": "Full",
|
||||
"high": "[%key:common::state::full%]",
|
||||
"mid": "[%key:common::state::medium%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"warning": "Empty"
|
||||
"warning": "[%key:common::state::empty%]"
|
||||
}
|
||||
},
|
||||
"relative_to_start": {
|
||||
|
@ -70,7 +70,7 @@
|
||||
"motor_fault_short": "Motor shorted",
|
||||
"motor_ot_amps": "Motor overtorqued",
|
||||
"motor_disconnected": "Motor disconnected",
|
||||
"empty": "Empty"
|
||||
"empty": "[%key:common::state::empty%]"
|
||||
}
|
||||
},
|
||||
"last_seen": {
|
||||
|
@ -200,7 +200,7 @@ async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, entry: LookinConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove lookin config entry from a device."""
|
||||
data: LookinData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
all_identifiers: set[tuple[str, str]] = {
|
||||
(DOMAIN, data.lookin_device.id),
|
||||
*((DOMAIN, remote["UUID"]) for remote in data.devices),
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.2.1"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
|
||||
}
|
||||
|
@ -7,6 +7,6 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==7.0.0"],
|
||||
"requirements": ["python-matter-server==8.0.0"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ from typing import Any, cast
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
|
||||
from matter_server.client.models import device_types
|
||||
from matter_server.common import custom_clusters
|
||||
|
||||
from homeassistant.components.number import (
|
||||
@ -18,6 +19,7 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfLength,
|
||||
@ -123,6 +125,31 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
)
|
||||
|
||||
|
||||
class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
"""Representation of a Matter Attribute as a Number entity."""
|
||||
|
||||
entity_description: MatterNumberEntityDescription
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set level value."""
|
||||
send_value = int(value)
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
send_value = value_convert(value)
|
||||
await self.send_device_command(
|
||||
clusters.LevelControl.Commands.MoveToLevel(
|
||||
level=send_value,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
@ -239,6 +266,26 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
vendor_id=(4874,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="pump_setpoint",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
translation_key="pump_setpoint",
|
||||
native_max_value=100,
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
measurement_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
),
|
||||
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
entity_class=MatterLevelControlNumber,
|
||||
required_attributes=(clusters.LevelControl.Attributes.CurrentLevel,),
|
||||
device_type=(device_types.Pump,),
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
|
@ -180,6 +180,9 @@
|
||||
"altitude": {
|
||||
"name": "Altitude above sea level"
|
||||
},
|
||||
"pump_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
|
@ -32,11 +32,18 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
llm_apis = {api.id: api.name for api in llm.async_get_apis(self.hass)}
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input
|
||||
)
|
||||
if not user_input[CONF_LLM_HASS_API]:
|
||||
errors[CONF_LLM_HASS_API] = "llm_api_required"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=", ".join(
|
||||
llm_apis[api_id] for api_id in user_input[CONF_LLM_HASS_API]
|
||||
),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@ -44,7 +51,7 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
default=llm.LLM_API_ASSIST,
|
||||
default=[llm.LLM_API_ASSIST],
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
@ -53,10 +60,12 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
value=llm_api_id,
|
||||
)
|
||||
for llm_api_id, name in llm_apis.items()
|
||||
]
|
||||
],
|
||||
multiple=True,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders={"more_info_url": MORE_INFO_URL},
|
||||
errors=errors,
|
||||
)
|
||||
|
@ -42,7 +42,7 @@ def _format_tool(
|
||||
|
||||
|
||||
async def create_server(
|
||||
hass: HomeAssistant, llm_api_id: str, llm_context: llm.LLMContext
|
||||
hass: HomeAssistant, llm_api_id: str | list[str], llm_context: llm.LLMContext
|
||||
) -> Server:
|
||||
"""Create a new Model Context Protocol Server.
|
||||
|
||||
|
@ -11,6 +11,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"llm_api_required": "At least one LLM API must be configured."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
|
@ -2,34 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from medcom_ble import MedcomBleDeviceData
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
|
||||
# Supported platforms
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Medcom BLE radiation monitor from a config entry."""
|
||||
|
||||
address = entry.unique_id
|
||||
elevation = hass.config.elevation
|
||||
is_metric = hass.config.units is METRIC_SYSTEM
|
||||
assert address is not None
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
@ -38,26 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"Could not find Medcom BLE device with address {address}"
|
||||
)
|
||||
|
||||
async def _async_update_method():
|
||||
"""Get data from Medcom BLE radiation monitor."""
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
inspector = MedcomBleDeviceData(_LOGGER, elevation, is_metric)
|
||||
|
||||
try:
|
||||
data = await inspector.update_device(ble_device)
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_method,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
coordinator = MedcomBleUpdateCoordinator(hass, entry, address)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
50
homeassistant/components/medcom_ble/coordinator.py
Normal file
50
homeassistant/components/medcom_ble/coordinator.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""The Medcom BLE integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from medcom_ble import MedcomBleDevice, MedcomBleDeviceData
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]):
|
||||
"""Coordinator for Medcom BLE radiation monitor data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._address = address
|
||||
self._elevation = hass.config.elevation
|
||||
self._is_metric = hass.config.units is METRIC_SYSTEM
|
||||
|
||||
async def _async_update_data(self) -> MedcomBleDevice:
|
||||
"""Get data from Medcom BLE radiation monitor."""
|
||||
ble_device = bluetooth.async_ble_device_from_address(self.hass, self._address)
|
||||
inspector = MedcomBleDeviceData(_LOGGER, self._elevation, self._is_metric)
|
||||
|
||||
try:
|
||||
data = await inspector.update_device(ble_device)
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from medcom_ble import MedcomBleDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@ -15,12 +13,10 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, UNIT_CPM
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -41,9 +37,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Medcom BLE radiation monitor sensors."""
|
||||
|
||||
coordinator: DataUpdateCoordinator[MedcomBleDevice] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities = []
|
||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||
@ -62,16 +56,14 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MedcomSensor(
|
||||
CoordinatorEntity[DataUpdateCoordinator[MedcomBleDevice]], SensorEntity
|
||||
):
|
||||
class MedcomSensor(CoordinatorEntity[MedcomBleUpdateCoordinator], SensorEntity):
|
||||
"""Medcom BLE radiation monitor sensors for the device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[MedcomBleDevice],
|
||||
coordinator: MedcomBleUpdateCoordinator,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Populate the medcom entity with relevant data."""
|
||||
|
@ -30,6 +30,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
MEDIA_CLASS_MAP,
|
||||
MEDIA_MIME_TYPES,
|
||||
MEDIA_SOURCE_DATA,
|
||||
URI_SCHEME,
|
||||
URI_SCHEME_REGEX,
|
||||
)
|
||||
@ -78,7 +79,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the media_source component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[MEDIA_SOURCE_DATA] = {}
|
||||
websocket_api.async_register_command(hass, websocket_browse_media)
|
||||
websocket_api.async_register_command(hass, websocket_resolve_media)
|
||||
frontend.async_register_built_in_panel(
|
||||
@ -97,7 +98,7 @@ async def _process_media_source_platform(
|
||||
platform: MediaSourceProtocol,
|
||||
) -> None:
|
||||
"""Process a media source platform."""
|
||||
hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass)
|
||||
hass.data[MEDIA_SOURCE_DATA][domain] = await platform.async_get_media_source(hass)
|
||||
|
||||
|
||||
@callback
|
||||
@ -109,10 +110,10 @@ def _get_media_item(
|
||||
item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player)
|
||||
else:
|
||||
# We default to our own domain if its only one registered
|
||||
domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN
|
||||
domain = None if len(hass.data[MEDIA_SOURCE_DATA]) > 1 else DOMAIN
|
||||
return MediaSourceItem(hass, domain, "", target_media_player)
|
||||
|
||||
if item.domain is not None and item.domain not in hass.data[DOMAIN]:
|
||||
if item.domain is not None and item.domain not in hass.data[MEDIA_SOURCE_DATA]:
|
||||
raise UnknownMediaSource(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unknown_media_source",
|
||||
|
@ -1,10 +1,18 @@
|
||||
"""Constants for the media_source integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.media_player import MediaClass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import MediaSource
|
||||
|
||||
DOMAIN = "media_source"
|
||||
MEDIA_SOURCE_DATA: HassKey[dict[str, MediaSource]] = HassKey(DOMAIN)
|
||||
MEDIA_MIME_TYPES = ("audio", "video", "image")
|
||||
MEDIA_CLASS_MAP = {
|
||||
"audio": MediaClass.MUSIC,
|
||||
|
@ -6,7 +6,7 @@ import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_request import FileField
|
||||
@ -18,7 +18,7 @@ from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, MEDIA_SOURCE_DATA
|
||||
from .error import Unresolvable
|
||||
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
||||
|
||||
@ -30,7 +30,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up local media source."""
|
||||
source = LocalSource(hass)
|
||||
hass.data[DOMAIN][DOMAIN] = source
|
||||
hass.data[MEDIA_SOURCE_DATA][DOMAIN] = source
|
||||
hass.http.register_view(LocalMediaView(hass, source))
|
||||
hass.http.register_view(UploadMediaView(hass, source))
|
||||
websocket_api.async_register_command(hass, websocket_remove_media)
|
||||
@ -352,7 +352,7 @@ async def websocket_remove_media(
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
return
|
||||
|
||||
source: LocalSource = hass.data[DOMAIN][DOMAIN]
|
||||
source = cast(LocalSource, hass.data[MEDIA_SOURCE_DATA][DOMAIN])
|
||||
|
||||
try:
|
||||
source_dir_id, location = source.async_parse_identifier(item)
|
||||
|
@ -3,12 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
|
||||
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@ -70,7 +70,7 @@ class MediaSourceItem:
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for source in self.hass.data[DOMAIN].values()
|
||||
for source in self.hass.data[MEDIA_SOURCE_DATA].values()
|
||||
),
|
||||
key=lambda item: item.title,
|
||||
)
|
||||
@ -85,7 +85,9 @@ class MediaSourceItem:
|
||||
@callback
|
||||
def async_media_source(self) -> MediaSource:
|
||||
"""Return media source that owns this item."""
|
||||
return cast(MediaSource, self.hass.data[DOMAIN][self.domain])
|
||||
if TYPE_CHECKING:
|
||||
assert self.domain is not None
|
||||
return self.hass.data[MEDIA_SOURCE_DATA][self.domain]
|
||||
|
||||
@classmethod
|
||||
def from_uri(
|
||||
|
@ -27,9 +27,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
type MelCloudConfigEntry = ConfigEntry[dict[str, list[MelCloudDevice]]]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Establish connection with MELClooud."""
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) -> bool:
|
||||
"""Establish connection with MELCloud."""
|
||||
conf = entry.data
|
||||
try:
|
||||
mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN])
|
||||
@ -40,20 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except (TimeoutError, ClientConnectionError) as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices})
|
||||
entry.runtime_data = mel_devices
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
class MelCloudDevice:
|
||||
|
@ -24,13 +24,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudDevice
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from .const import (
|
||||
ATTR_STATUS,
|
||||
ATTR_VANE_HORIZONTAL,
|
||||
@ -38,7 +37,6 @@ from .const import (
|
||||
ATTR_VANE_VERTICAL,
|
||||
ATTR_VANE_VERTICAL_POSITIONS,
|
||||
CONF_POSITION,
|
||||
DOMAIN,
|
||||
SERVICE_SET_VANE_HORIZONTAL,
|
||||
SERVICE_SET_VANE_VERTICAL,
|
||||
)
|
||||
@ -77,11 +75,11 @@ ATW_ZONE_HVAC_ACTION_LOOKUP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MelCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MelCloud device climate based on config_entry."""
|
||||
mel_devices = hass.data[DOMAIN][entry.entry_id]
|
||||
mel_devices = entry.runtime_data
|
||||
entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [
|
||||
AtaDeviceClimate(mel_device, mel_device.device)
|
||||
for mel_device in mel_devices[DEVICE_TYPE_ATA]
|
||||
|
@ -5,11 +5,12 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import MelCloudConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_USERNAME,
|
||||
CONF_TOKEN,
|
||||
@ -17,7 +18,7 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: MelCloudConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for the config entry."""
|
||||
ent_reg = er.async_get(hass)
|
||||
|
@ -15,13 +15,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudDevice
|
||||
from .const import DOMAIN
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
@ -105,11 +103,11 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MelCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MELCloud device sensors based on config_entry."""
|
||||
mel_devices = hass.data[DOMAIN].get(entry.entry_id)
|
||||
mel_devices = entry.runtime_data
|
||||
|
||||
entities: list[MelDeviceSensor] = [
|
||||
MelDeviceSensor(mel_device, description)
|
||||
|
@ -17,22 +17,21 @@ from homeassistant.components.water_heater import (
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, MelCloudDevice
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from .const import ATTR_STATUS
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MelCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MelCloud device climate based on config_entry."""
|
||||
mel_devices = hass.data[DOMAIN][entry.entry_id]
|
||||
mel_devices = entry.runtime_data
|
||||
async_add_entities(
|
||||
[
|
||||
AtwWaterHeater(mel_device, mel_device.device)
|
||||
|
@ -6,13 +6,11 @@ from melnor_bluetooth.device import Device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
@ -22,11 +20,8 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
|
||||
"""Set up melnor from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS])
|
||||
|
||||
if not ble_device:
|
||||
@ -60,20 +55,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = MelnorDataUpdateCoordinator(hass, entry, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.data.disconnect()
|
||||
|
||||
device: Device = hass.data[DOMAIN][entry.entry_id].data
|
||||
|
||||
await device.disconnect()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -11,15 +11,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MelnorConfigEntry = ConfigEntry[MelnorDataUpdateCoordinator]
|
||||
|
||||
|
||||
class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
"""Melnor data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MelnorConfigEntry
|
||||
_device: Device
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
|
||||
self, hass: HomeAssistant, config_entry: MelnorConfigEntry, device: Device
|
||||
) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
|
@ -13,13 +13,11 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@ -67,12 +65,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the number platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@ -26,8 +25,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorBluetoothEntity, MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@ -104,12 +102,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
# Device-level sensors
|
||||
async_add_entities(
|
||||
|
@ -13,12 +13,10 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@ -51,12 +49,12 @@ ZONE_ENTITY_DESCRIPTIONS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
@ -10,13 +10,11 @@ from typing import Any
|
||||
from melnor_bluetooth.device import Valve
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@ -41,12 +39,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneTimeEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the number platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
@ -1,59 +1,21 @@
|
||||
"""The met_eireann component."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Self
|
||||
|
||||
import meteireann
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||
from .coordinator import MetEireannUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.WEATHER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Met Éireann as config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
raw_weather_data = meteireann.WeatherData(
|
||||
async_get_clientsession(hass),
|
||||
latitude=config_entry.data[CONF_LATITUDE],
|
||||
longitude=config_entry.data[CONF_LONGITUDE],
|
||||
altitude=config_entry.data[CONF_ELEVATION],
|
||||
)
|
||||
|
||||
weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
|
||||
|
||||
async def _async_update_data() -> MetEireannWeatherData:
|
||||
"""Fetch data from Met Éireann."""
|
||||
try:
|
||||
return await weather_data.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_data,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
coordinator = MetEireannUpdateCoordinator(hass, config_entry=config_entry)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@ -68,26 +30,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class MetEireannWeatherData:
|
||||
"""Keep data for Met Éireann weather entities."""
|
||||
|
||||
def __init__(
|
||||
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
|
||||
) -> None:
|
||||
"""Initialise the weather entity data."""
|
||||
self._config = config
|
||||
self._weather_data = weather_data
|
||||
self.current_weather_data: dict[str, Any] = {}
|
||||
self.daily_forecast: list[dict[str, Any]] = []
|
||||
self.hourly_forecast: list[dict[str, Any]] = []
|
||||
|
||||
async def fetch_data(self) -> Self:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
76
homeassistant/components/met_eireann/coordinator.py
Normal file
76
homeassistant/components/met_eireann/coordinator.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""The met_eireann component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Self
|
||||
|
||||
import meteireann
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||
|
||||
|
||||
class MetEireannWeatherData:
|
||||
"""Keep data for Met Éireann weather entities."""
|
||||
|
||||
def __init__(
|
||||
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
|
||||
) -> None:
|
||||
"""Initialise the weather entity data."""
|
||||
self._config = config
|
||||
self._weather_data = weather_data
|
||||
self.current_weather_data: dict[str, Any] = {}
|
||||
self.daily_forecast: list[dict[str, Any]] = []
|
||||
self.hourly_forecast: list[dict[str, Any]] = []
|
||||
|
||||
async def fetch_data(self) -> Self:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
||||
|
||||
class MetEireannUpdateCoordinator(DataUpdateCoordinator[MetEireannWeatherData]):
|
||||
"""Coordinator for Met Éireann weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
raw_weather_data = meteireann.WeatherData(
|
||||
async_get_clientsession(hass),
|
||||
latitude=config_entry.data[CONF_LATITUDE],
|
||||
longitude=config_entry.data[CONF_LONGITUDE],
|
||||
altitude=config_entry.data[CONF_ELEVATION],
|
||||
)
|
||||
self._weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
|
||||
|
||||
async def _async_update_data(self) -> MetEireannWeatherData:
|
||||
"""Fetch data from Met Éireann."""
|
||||
try:
|
||||
return await self._weather_data.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
@ -1,7 +1,6 @@
|
||||
"""Support for Met Éireann weather service."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
@ -29,10 +28,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MetEireannWeatherData
|
||||
from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .coordinator import MetEireannWeatherData
|
||||
|
||||
|
||||
def format_condition(condition: str | None) -> str | None:
|
||||
|
@ -23,7 +23,6 @@ from .const import (
|
||||
COORDINATOR_RAIN,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -130,10 +129,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.title,
|
||||
)
|
||||
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
COORDINATOR_FORECAST: coordinator_forecast,
|
||||
}
|
||||
if coordinator_rain and coordinator_rain.last_update_success:
|
||||
@ -163,7 +161,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
@ -26,7 +26,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
COORDINATOR_FORECAST = "coordinator_forecast"
|
||||
COORDINATOR_RAIN = "coordinator_rain"
|
||||
COORDINATOR_ALERT = "coordinator_alert"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
ATTRIBUTION = "Data provided by Météo-France"
|
||||
MODEL = "Météo-France mobile API"
|
||||
MANUFACTURER = "Météo-France"
|
||||
|
@ -6,6 +6,8 @@ import time
|
||||
from meteofrance_api.model.forecast import Forecast as MeteoFranceForecast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_HUMIDITY,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||
@ -49,9 +51,13 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_condition(condition: str):
|
||||
def format_condition(condition: str, force_day: bool = False) -> str:
|
||||
"""Return condition from dict CONDITION_MAP."""
|
||||
return CONDITION_MAP.get(condition, condition)
|
||||
mapped_condition = CONDITION_MAP.get(condition, condition)
|
||||
if force_day and mapped_condition == ATTR_CONDITION_CLEAR_NIGHT:
|
||||
# Meteo-France can return clear night condition instead of sunny for daily weather, so we map it to sunny
|
||||
return ATTR_CONDITION_SUNNY
|
||||
return mapped_condition
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -212,7 +218,7 @@ class MeteoFranceWeather(
|
||||
forecast["dt"]
|
||||
).isoformat(),
|
||||
ATTR_FORECAST_CONDITION: format_condition(
|
||||
forecast["weather12H"]["desc"]
|
||||
forecast["weather12H"]["desc"], force_day=True
|
||||
),
|
||||
ATTR_FORECAST_HUMIDITY: forecast["humidity"]["max"],
|
||||
ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"],
|
||||
|
@ -1,43 +1,15 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
import logging
|
||||
|
||||
from meteoclimatic import MeteoclimaticClient
|
||||
from meteoclimatic.exceptions import MeteoclimaticError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Meteoclimatic entry."""
|
||||
station_code = entry.data[CONF_STATION_CODE]
|
||||
meteoclimatic_client = MeteoclimaticClient()
|
||||
|
||||
async def async_update_data():
|
||||
"""Obtain the latest data from Meteoclimatic."""
|
||||
try:
|
||||
data = await hass.async_add_executor_job(
|
||||
meteoclimatic_client.weather_at_station, station_code
|
||||
)
|
||||
except MeteoclimaticError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
return data.__dict__
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meteoclimatic weather for {entry.title} ({station_code})",
|
||||
update_method=async_update_data,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
coordinator = MeteoclimaticUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
43
homeassistant/components/meteoclimatic/coordinator.py
Normal file
43
homeassistant/components/meteoclimatic/coordinator.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from meteoclimatic import MeteoclimaticClient
|
||||
from meteoclimatic.exceptions import MeteoclimaticError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_STATION_CODE, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for Meteoclimatic weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self._station_code = entry.data[CONF_STATION_CODE]
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meteoclimatic weather for {entry.title} ({self._station_code})",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self._meteoclimatic_client = MeteoclimaticClient()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Obtain the latest data from Meteoclimatic."""
|
||||
try:
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self._meteoclimatic_client.weather_at_station, self._station_code
|
||||
)
|
||||
except MeteoclimaticError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
return data.__dict__
|
@ -18,12 +18,10 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@ -119,7 +117,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
[MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES],
|
||||
@ -127,13 +125,17 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class MeteoclimaticSensor(CoordinatorEntity, SensorEntity):
|
||||
class MeteoclimaticSensor(
|
||||
CoordinatorEntity[MeteoclimaticUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Representation of a Meteoclimatic sensor."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
|
||||
def __init__(
|
||||
self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription
|
||||
self,
|
||||
coordinator: MeteoclimaticUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Meteoclimatic sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
@ -8,12 +8,10 @@ from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
def format_condition(condition):
|
||||
@ -31,12 +29,14 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic weather platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([MeteoclimaticWeather(coordinator)], False)
|
||||
|
||||
|
||||
class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
|
||||
class MeteoclimaticWeather(
|
||||
CoordinatorEntity[MeteoclimaticUpdateCoordinator], WeatherEntity
|
||||
):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
@ -44,7 +44,7 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
|
||||
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
|
||||
def __init__(self, coordinator: MeteoclimaticUpdateCoordinator) -> None:
|
||||
"""Initialise the weather platform."""
|
||||
super().__init__(coordinator)
|
||||
self._unique_id = self.coordinator.data["station"].code
|
||||
|
@ -9,6 +9,7 @@ from datapoint.Forecast import Forecast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
EntityCategory,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
@ -59,6 +60,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = (
|
||||
native_attr_name="name",
|
||||
name="Station name",
|
||||
icon="mdi:label-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
MetOfficeSensorEntityDescription(
|
||||
@ -235,14 +237,13 @@ class MetOfficeCurrentSensor(
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
value = get_attribute(
|
||||
self.coordinator.data.now(), self.entity_description.native_attr_name
|
||||
)
|
||||
native_attr = self.entity_description.native_attr_name
|
||||
|
||||
if (
|
||||
self.entity_description.native_attr_name == "significantWeatherCode"
|
||||
and value is not None
|
||||
):
|
||||
if native_attr == "name":
|
||||
return str(self.coordinator.data.name)
|
||||
|
||||
value = get_attribute(self.coordinator.data.now(), native_attr)
|
||||
if native_attr == "significantWeatherCode" and value is not None:
|
||||
value = CONDITION_MAP.get(value)
|
||||
|
||||
return value
|
||||
|
@ -172,7 +172,7 @@ async def async_modbus_setup(
|
||||
|
||||
async def async_write_register(service: ServiceCall) -> None:
|
||||
"""Write Modbus registers."""
|
||||
slave = 0
|
||||
slave = 1
|
||||
if ATTR_UNIT in service.data:
|
||||
slave = int(float(service.data[ATTR_UNIT]))
|
||||
|
||||
@ -195,7 +195,7 @@ async def async_modbus_setup(
|
||||
|
||||
async def async_write_coil(service: ServiceCall) -> None:
|
||||
"""Write Modbus coil."""
|
||||
slave = 0
|
||||
slave = 1
|
||||
if ATTR_UNIT in service.data:
|
||||
slave = int(float(service.data[ATTR_UNIT]))
|
||||
if ATTR_SLAVE in service.data:
|
||||
|
@ -10,13 +10,7 @@ from homeassistant.const import CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import (
|
||||
CONF_NOT_FIRST_RUN,
|
||||
DOMAIN,
|
||||
FIRST_RUN,
|
||||
MONOPRICE_OBJECT,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from .const import CONF_NOT_FIRST_RUN, DOMAIN, FIRST_RUN, MONOPRICE_OBJECT
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
@ -41,11 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry, data={**entry.data, CONF_NOT_FIRST_RUN: True}
|
||||
)
|
||||
|
||||
undo_listener = entry.add_update_listener(_update_listener)
|
||||
entry.async_on_unload(entry.add_update_listener(_update_listener))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
MONOPRICE_OBJECT: monoprice,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
FIRST_RUN: first_run,
|
||||
}
|
||||
|
||||
@ -60,8 +53,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not unload_ok:
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
def _cleanup(monoprice) -> None:
|
||||
"""Destroy the Monoprice object.
|
||||
|
||||
|
@ -18,4 +18,3 @@ SERVICE_RESTORE = "restore"
|
||||
|
||||
FIRST_RUN = "first_run"
|
||||
MONOPRICE_OBJECT = "monoprice_object"
|
||||
UNDO_UPDATE_LISTENER = "update_update_listener"
|
||||
|
@ -2771,11 +2771,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
reconfig=True,
|
||||
)
|
||||
if user_input is not None:
|
||||
new_device_data, errors = validate_user_input(
|
||||
user_input, MQTT_DEVICE_PLATFORM_FIELDS
|
||||
)
|
||||
if "mqtt_settings" in user_input:
|
||||
new_device_data["mqtt_settings"] = user_input["mqtt_settings"]
|
||||
new_device_data: dict[str, Any] = user_input.copy()
|
||||
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
|
||||
if "advanced_settings" in new_device_data:
|
||||
new_device_data |= new_device_data.pop("advanced_settings")
|
||||
if not errors:
|
||||
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, new_device_data)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
|
@ -34,7 +34,7 @@ class MusicAssistantEntity(Entity):
|
||||
identifiers={(DOMAIN, player_id)},
|
||||
manufacturer=self.player.device_info.manufacturer or provider.name,
|
||||
model=self.player.device_info.model or self.player.name,
|
||||
name=self.player.display_name,
|
||||
name=self.player.name,
|
||||
configuration_url=f"{mass.server_url}/#/settings/editplayer/{player_id}",
|
||||
)
|
||||
|
||||
|
@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["music_assistant"],
|
||||
"requirements": ["music-assistant-client==1.2.0"],
|
||||
"requirements": ["music-assistant-client==1.2.4"],
|
||||
"zeroconf": ["_mass._tcp.local."]
|
||||
}
|
||||
|
@ -6,11 +6,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from music_assistant_models.enums import MediaType as MASSMediaType
|
||||
from music_assistant_models.media_items import (
|
||||
BrowseFolder,
|
||||
MediaItemType,
|
||||
SearchResults,
|
||||
)
|
||||
from music_assistant_models.media_items import MediaItemType, SearchResults
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
@ -549,8 +545,6 @@ def _process_search_results(
|
||||
|
||||
# Add available items to results
|
||||
for item in items:
|
||||
if TYPE_CHECKING:
|
||||
assert not isinstance(item, BrowseFolder)
|
||||
if not item.available:
|
||||
continue
|
||||
|
||||
|
@ -248,10 +248,8 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
player = self.player
|
||||
active_queue = self.active_queue
|
||||
# update generic attributes
|
||||
if player.powered and active_queue is not None:
|
||||
self._attr_state = MediaPlayerState(active_queue.state.value)
|
||||
if player.powered and player.state is not None:
|
||||
self._attr_state = MediaPlayerState(player.state.value)
|
||||
if player.powered and player.playback_state is not None:
|
||||
self._attr_state = MediaPlayerState(player.playback_state.value)
|
||||
else:
|
||||
self._attr_state = MediaPlayerState(STATE_OFF)
|
||||
# active source and source list (translate to HA source names)
|
||||
@ -270,12 +268,12 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
self._attr_source = active_source_name
|
||||
|
||||
group_members: list[str] = []
|
||||
if player.group_childs:
|
||||
group_members = player.group_childs
|
||||
if player.group_members:
|
||||
group_members = player.group_members
|
||||
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
|
||||
group_members = parent.group_childs
|
||||
group_members = parent.group_members
|
||||
|
||||
# translate MA group_childs to HA group_members as entity id's
|
||||
# translate MA group_members to HA group_members as entity id's
|
||||
entity_registry = er.async_get(self.hass)
|
||||
group_members_entity_ids: list[str] = [
|
||||
entity_id
|
||||
|
@ -9,13 +9,11 @@ from pymystrom.bulb import MyStromBulb
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
from pymystrom.switch import MyStromSwitch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import MyStromData
|
||||
from .models import MyStromConfigEntry, MyStromData
|
||||
|
||||
PLATFORMS_PLUGS = [Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS_BULB = [Platform.LIGHT]
|
||||
@ -41,7 +39,7 @@ def _get_mystrom_switch(host: str) -> MyStromSwitch:
|
||||
return MyStromSwitch(host)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool:
|
||||
"""Set up myStrom from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
try:
|
||||
@ -73,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.error("Unsupported myStrom device type: %s", device_type)
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData(
|
||||
entry.runtime_data = MyStromData(
|
||||
device=device,
|
||||
info=info,
|
||||
)
|
||||
@ -82,15 +80,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
device_type = hass.data[DOMAIN][entry.entry_id].info["type"]
|
||||
device_type = entry.runtime_data.info["type"]
|
||||
platforms = []
|
||||
if device_type in [101, 106, 107, 120]:
|
||||
platforms.extend(PLATFORMS_PLUGS)
|
||||
elif device_type in [102, 105]:
|
||||
platforms.extend(PLATFORMS_BULB)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, platforms)
|
||||
|
@ -15,12 +15,12 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .models import MyStromConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -32,12 +32,12 @@ EFFECT_SUNRISE = "sunrise"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MyStromConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the myStrom entities."""
|
||||
info = hass.data[DOMAIN][entry.entry_id].info
|
||||
device = hass.data[DOMAIN][entry.entry_id].device
|
||||
info = entry.runtime_data.info
|
||||
device = entry.runtime_data.device
|
||||
async_add_entities([MyStromLight(device, entry.title, info["mac"])])
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user