bump some more

This commit is contained in:
J. Nick Koston 2025-07-03 09:55:08 -05:00
commit a60c2d840f
No known key found for this signature in database
258 changed files with 5501 additions and 1899 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -209,6 +209,12 @@
"state": {
"off": "mdi:card-bulleted-off-outline"
}
},
"boost": {
"default": "mdi:thermometer-high",
"state": {
"off": "mdi:thermometer-off"
}
}
}
}

View File

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

View File

@ -278,6 +278,9 @@
},
"calibrate_cjc": {
"name": "Calibrate CJC"
},
"boost": {
"name": "Boost"
}
}
},

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -180,6 +180,9 @@
"altitude": {
"name": "Altitude above sea level"
},
"pump_setpoint": {
"name": "Setpoint"
},
"temperature_offset": {
"name": "Temperature offset"
},

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,4 +18,3 @@ SERVICE_RESTORE = "restore"
FIRST_RUN = "first_run"
MONOPRICE_OBJECT = "monoprice_object"
UNDO_UPDATE_LISTENER = "update_update_listener"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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