mirror of
https://github.com/home-assistant/core.git
synced 2025-12-27 02:08:31 +00:00
Compare commits
224 Commits
enforce_en
...
state_temp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be1ba31ff2 | ||
|
|
3c4ecffa1b | ||
|
|
244e0f5ea8 | ||
|
|
a656b6e26a | ||
|
|
691681a78a | ||
|
|
3bc00824e2 | ||
|
|
7d36a2e3a7 | ||
|
|
b1e3561ead | ||
|
|
bfc814c839 | ||
|
|
5008151688 | ||
|
|
d738c0d6b1 | ||
|
|
e42235285d | ||
|
|
04e69479f4 | ||
|
|
b973916032 | ||
|
|
6f4757ef42 | ||
|
|
a6962e9e1e | ||
|
|
142c10cccc | ||
|
|
c137c96cfd | ||
|
|
f0e0c954e7 | ||
|
|
681961d3a5 | ||
|
|
53d2f6b0c6 | ||
|
|
78c39f8a06 | ||
|
|
a748525e03 | ||
|
|
8ca1fe83b7 | ||
|
|
8968cf704b | ||
|
|
ebe04466f4 | ||
|
|
e31470ba5b | ||
|
|
4bc2951f44 | ||
|
|
8334a0398c | ||
|
|
80a1e0e4cd | ||
|
|
3778f537d5 | ||
|
|
adec157d43 | ||
|
|
8fc3fa51a8 | ||
|
|
4eb688b560 | ||
|
|
9472ff5d36 | ||
|
|
12e8b81ec7 | ||
|
|
ec5e543c09 | ||
|
|
116c745872 | ||
|
|
1fdf152292 | ||
|
|
b816f1a408 | ||
|
|
eb351e6505 | ||
|
|
2f27d55495 | ||
|
|
fa1bed1849 | ||
|
|
b8c19f23f3 | ||
|
|
b677ce6c90 | ||
|
|
d6da686ffe | ||
|
|
f50ef79c72 | ||
|
|
943fb9948b | ||
|
|
7447cf329b | ||
|
|
3d27c0ce52 | ||
|
|
b7496be61f | ||
|
|
57a98240bd | ||
|
|
ff76017ba6 | ||
|
|
f10fcde6d8 | ||
|
|
a7002e3a24 | ||
|
|
bbe03dcab7 | ||
|
|
f77e6cc8fc | ||
|
|
cb8e076703 | ||
|
|
73251fbb1c | ||
|
|
7ff90ca49d | ||
|
|
bab9ec9976 | ||
|
|
1051f85ac0 | ||
|
|
6c7da57af2 | ||
|
|
73e505d48d | ||
|
|
ec65066f5e | ||
|
|
9c4951261c | ||
|
|
00dfc04b86 | ||
|
|
bee07ad284 | ||
|
|
b2108fdd40 | ||
|
|
3730a1a379 | ||
|
|
088c02d38a | ||
|
|
afb247c907 | ||
|
|
0e6bbb30c1 | ||
|
|
fdba791f18 | ||
|
|
d4dec6c7a9 | ||
|
|
f838e85a79 | ||
|
|
04ae966544 | ||
|
|
77dcba0984 | ||
|
|
48f9a12cca | ||
|
|
bdd2ac9ae4 | ||
|
|
2e7113d881 | ||
|
|
6842bfae4c | ||
|
|
392cde20d9 | ||
|
|
a6146fb5a9 | ||
|
|
b2c393db72 | ||
|
|
6104731d53 | ||
|
|
3ed440a3af | ||
|
|
01e7efc7b4 | ||
|
|
60a930554a | ||
|
|
66308a848a | ||
|
|
c71dbd9d4d | ||
|
|
1195c2ec10 | ||
|
|
78a9cd9201 | ||
|
|
639a749a0f | ||
|
|
058f3b8b6e | ||
|
|
926e9261ab | ||
|
|
d6fb860889 | ||
|
|
5e03900e0a | ||
|
|
1e6e5ca1b6 | ||
|
|
60e3b38de1 | ||
|
|
852522219c | ||
|
|
23f1e8d1a3 | ||
|
|
c707bf6264 | ||
|
|
655f009f07 | ||
|
|
3548ab70fd | ||
|
|
e272ab1885 | ||
|
|
d5d1b620d0 | ||
|
|
8b2f4f0f86 | ||
|
|
725269ecda | ||
|
|
c42fc818bf | ||
|
|
5554e38171 | ||
|
|
b25acfe823 | ||
|
|
ff25948e37 | ||
|
|
f85fc7173f | ||
|
|
748cc6386d | ||
|
|
47b232db49 | ||
|
|
c61935fc41 | ||
|
|
414318f3fb | ||
|
|
08985d783f | ||
|
|
e4bcde7d20 | ||
|
|
59bf39f4ed | ||
|
|
510e3977df | ||
|
|
922720576a | ||
|
|
e10b581d4b | ||
|
|
e38eac9415 | ||
|
|
11c9aa9280 | ||
|
|
52c86f8a6a | ||
|
|
6364a9ad98 | ||
|
|
651162b8e7 | ||
|
|
7deca35172 | ||
|
|
073a467fb2 | ||
|
|
3f9590b03b | ||
|
|
b47f989c77 | ||
|
|
4ebffa8d23 | ||
|
|
c5873c6dd0 | ||
|
|
2cb80e083e | ||
|
|
871296dff6 | ||
|
|
db04c77e62 | ||
|
|
e8204e5f8e | ||
|
|
66cf9c4ed5 | ||
|
|
1f6d28dcbf | ||
|
|
328e838351 | ||
|
|
62a1c8af11 | ||
|
|
b50e599517 | ||
|
|
3c7c9176d2 | ||
|
|
c771f5fe1e | ||
|
|
6dc464ad73 | ||
|
|
ae48e3716e | ||
|
|
1543726095 | ||
|
|
adbace95c3 | ||
|
|
578b43cf61 | ||
|
|
a8b5d1511d | ||
|
|
5a0a1bbbf4 | ||
|
|
cf2e69ed74 | ||
|
|
c32b44b774 | ||
|
|
2f69ed4a8a | ||
|
|
4b3449fe0c | ||
|
|
33e1c6de68 | ||
|
|
81e712ea49 | ||
|
|
d3c5684cd0 | ||
|
|
862b7460b5 | ||
|
|
a65eb57539 | ||
|
|
b537850f52 | ||
|
|
16c6bd08f8 | ||
|
|
18834849c2 | ||
|
|
e4d820799f | ||
|
|
013a35176a | ||
|
|
8230557aef | ||
|
|
5451063714 | ||
|
|
8cdc7523a4 | ||
|
|
77ccfbd3a9 | ||
|
|
4977ee4998 | ||
|
|
5c0f2d37f0 | ||
|
|
0b5d2ab8e4 | ||
|
|
47f3bf29dd | ||
|
|
62f7cbb51e | ||
|
|
b9e2c5d34c | ||
|
|
1829acd0e1 | ||
|
|
41b9a7a9a3 | ||
|
|
9782637ec8 | ||
|
|
6bd6fa65d2 | ||
|
|
85343a9f53 | ||
|
|
bc607dd013 | ||
|
|
c2c388e0cc | ||
|
|
3fc154e1d7 | ||
|
|
efb29d024e | ||
|
|
263823c92c | ||
|
|
e5e6ed601b | ||
|
|
28dfc997f3 | ||
|
|
f93ab8d519 | ||
|
|
cb359da79e | ||
|
|
6a7385590a | ||
|
|
c0ec987b07 | ||
|
|
26521f8cc0 | ||
|
|
4df1f702bf | ||
|
|
c8422c9fb8 | ||
|
|
f8207a2e0e | ||
|
|
9cc75f3458 | ||
|
|
a233b6b1e3 | ||
|
|
c7677b91da | ||
|
|
1f57bba9cd | ||
|
|
4cc10ca2e2 | ||
|
|
153e1e43e8 | ||
|
|
398dd3ae46 | ||
|
|
17fd850fa6 | ||
|
|
ae062b230c | ||
|
|
d523f85404 | ||
|
|
f28d6582c6 | ||
|
|
1e81e5990e | ||
|
|
5fe2e4b6ed | ||
|
|
914bb3aa76 | ||
|
|
cfa6746115 | ||
|
|
03f9caf3eb | ||
|
|
6b2aaf3fdb | ||
|
|
2c4ea0d584 | ||
|
|
e627811f7a | ||
|
|
150f41641b | ||
|
|
b9a7371996 | ||
|
|
7d0e99da43 | ||
|
|
71f281cc14 | ||
|
|
aec812a475 | ||
|
|
d4b548b169 | ||
|
|
a296324c30 | ||
|
|
cff3d3d6ac |
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -452,8 +452,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
|
||||
/homeassistant/components/escea/ @lazdavila
|
||||
/tests/components/escea/ @lazdavila
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/event/ @home-assistant/core
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Uninstall pre-installed formatting and linting tools
|
||||
# They would conflict with our pinned versions
|
||||
RUN \
|
||||
pipx uninstall pydocstyle \
|
||||
&& pipx uninstall pycodestyle \
|
||||
&& pipx uninstall mypy \
|
||||
&& pipx uninstall pylint
|
||||
|
||||
RUN \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
@@ -32,21 +24,18 @@ RUN \
|
||||
libxml2 \
|
||||
git \
|
||||
cmake \
|
||||
autoconf \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
&& uv pip install --system -e hass-release/ \
|
||||
&& chown -R vscode /usr/src/hass-release/data
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
RUN uv python install 3.13.2
|
||||
|
||||
USER vscode
|
||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||
@@ -55,6 +44,10 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||
&& uv pip install -e ~/hass-release/
|
||||
|
||||
# Install Python dependencies from requirements
|
||||
COPY requirements.txt ./
|
||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||
@@ -65,4 +58,4 @@ RUN uv pip install -r requirements_test.txt
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
||||
ENV SHELL=/bin/bash
|
||||
|
||||
@@ -2,19 +2,45 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
api = AmazonEchoApi(
|
||||
data[CONF_COUNTRY],
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
data = await api.login_mode_interactive(data[CONF_CODE])
|
||||
finally:
|
||||
await api.close()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
@@ -25,17 +51,14 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
client = AmazonEchoApi(
|
||||
user_input[CONF_COUNTRY],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
||||
data = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -44,8 +67,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=user_input | {CONF_LOGIN_DATA: data},
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -61,3 +82,43 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth flow."""
|
||||
self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]}
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth confirm."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
entry_data = reauth_entry.data
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data={
|
||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||
CONF_CODE: user_input[CONF_CODE],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]},
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ from aioamazondevices.exceptions import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
@@ -55,4 +55,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryError("Could not authenticate") from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.22"]
|
||||
"requirements": ["aioamazondevices==3.2.2"]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: all tests missing
|
||||
|
||||
@@ -22,17 +22,29 @@
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.104.0"],
|
||||
"requirements": ["hass-nabucasa==0.105.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,11 +6,18 @@ from operator import itemgetter
|
||||
import numpy as np
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@@ -50,20 +57,23 @@ def datapoints_greater_than_degree(value: dict) -> dict:
|
||||
|
||||
COMPENSATION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Required(CONF_DATAPOINTS): [
|
||||
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
|
||||
],
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=7),
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,15 +7,23 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -59,24 +67,13 @@ async def async_setup_platform(
|
||||
|
||||
source: str = conf[CONF_SOURCE]
|
||||
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
if not (name := conf.get(CONF_NAME)):
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
CompensationSensor(
|
||||
conf.get(CONF_UNIQUE_ID),
|
||||
name,
|
||||
source,
|
||||
attribute,
|
||||
conf[CONF_PRECISION],
|
||||
conf[CONF_POLYNOMIAL],
|
||||
conf.get(CONF_UNIT_OF_MEASUREMENT),
|
||||
conf[CONF_MINIMUM],
|
||||
conf[CONF_MAXIMUM],
|
||||
)
|
||||
]
|
||||
[CompensationSensor(conf.get(CONF_UNIQUE_ID), name, source, attribute, conf)]
|
||||
)
|
||||
|
||||
|
||||
@@ -91,23 +88,27 @@ class CompensationSensor(SensorEntity):
|
||||
name: str,
|
||||
source: str,
|
||||
attribute: str | None,
|
||||
precision: int,
|
||||
polynomial: np.poly1d,
|
||||
unit_of_measurement: str | None,
|
||||
minimum: tuple[float, float] | None,
|
||||
maximum: tuple[float, float] | None,
|
||||
config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize the Compensation sensor."""
|
||||
|
||||
self._attr_name = name
|
||||
self._source_entity_id = source
|
||||
self._precision = precision
|
||||
self._source_attribute = attribute
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
|
||||
self._precision = config[CONF_PRECISION]
|
||||
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
|
||||
self._poly = polynomial
|
||||
self._coefficients = polynomial.coefficients.tolist()
|
||||
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_name = name
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._minimum = config[CONF_MINIMUM]
|
||||
self._maximum = config[CONF_MAXIMUM]
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_state_class = config.get(CONF_STATE_CLASS)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle added to Hass."""
|
||||
@@ -137,13 +138,40 @@ class CompensationSensor(SensorEntity):
|
||||
"""Handle sensor state changes."""
|
||||
new_state: State | None
|
||||
if (new_state := event.data["new_state"]) is None:
|
||||
_LOGGER.warning(
|
||||
"While updating compensation %s, the new_state is None", self.name
|
||||
)
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if new_state.state == STATE_UNAVAILABLE:
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_available = True
|
||||
|
||||
if self.native_unit_of_measurement is None and self._source_attribute is None:
|
||||
self._attr_native_unit_of_measurement = new_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
|
||||
if self._attr_device_class is None and (
|
||||
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
):
|
||||
self._attr_device_class = device_class
|
||||
|
||||
if self._attr_state_class is None and (
|
||||
state_class := new_state.attributes.get(ATTR_STATE_CLASS)
|
||||
):
|
||||
self._attr_state_class = state_class
|
||||
|
||||
if self._source_attribute:
|
||||
value = new_state.attributes.get(self._source_attribute)
|
||||
else:
|
||||
|
||||
@@ -19,10 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
SUPPORT_MINIMAL_SERVICES = VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF
|
||||
|
||||
SUPPORT_BASIC_SERVICES = (
|
||||
VacuumEntityFeature.STATE
|
||||
| VacuumEntityFeature.START
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.BATTERY
|
||||
VacuumEntityFeature.STATE | VacuumEntityFeature.START | VacuumEntityFeature.STOP
|
||||
)
|
||||
|
||||
SUPPORT_MOST_SERVICES = (
|
||||
@@ -31,7 +28,6 @@ SUPPORT_MOST_SERVICES = (
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.FAN_SPEED
|
||||
)
|
||||
|
||||
@@ -46,7 +42,6 @@ SUPPORT_ALL_SERVICES = (
|
||||
| VacuumEntityFeature.SEND_COMMAND
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.STATUS
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.MAP
|
||||
| VacuumEntityFeature.CLEAN_SPOT
|
||||
@@ -90,12 +85,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
self._attr_activity = VacuumActivity.DOCKED
|
||||
self._fan_speed = FAN_SPEEDS[1]
|
||||
self._cleaned_area: float = 0
|
||||
self._battery_level = 100
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int:
|
||||
"""Return the current battery level of the vacuum."""
|
||||
return max(0, min(100, self._battery_level))
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str:
|
||||
@@ -117,7 +106,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
if self._attr_activity != VacuumActivity.CLEANING:
|
||||
self._attr_activity = VacuumActivity.CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def pause(self) -> None:
|
||||
@@ -142,7 +130,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
"""Perform a spot clean-up."""
|
||||
self._attr_activity = VacuumActivity.CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.2.1"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["eheimdigital==1.2.0"],
|
||||
"requirements": ["eheimdigital==1.3.0"],
|
||||
"zeroconf": [
|
||||
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -281,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
_has_state: bool = False
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "esphome",
|
||||
"name": "ESPHome",
|
||||
"after_dependencies": ["hassio", "zeroconf", "tag"],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz", "@kbx81", "@bdraco"],
|
||||
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["assist_pipeline", "bluetooth", "intent", "ffmpeg", "http"],
|
||||
"dhcp": [
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==33.1.1",
|
||||
"aioesphomeapi==34.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250627.0"]
|
||||
"requirements": ["home-assistant-frontend==20250702.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["av==13.1.0", "Pillow==11.2.1"]
|
||||
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -330,13 +330,14 @@ async def google_generative_ai_config_option_schema(
|
||||
api_models = [api_model async for api_model in api_models_pager]
|
||||
models = [
|
||||
SelectOptionDict(
|
||||
label=api_model.display_name,
|
||||
label=api_model.name.lstrip("models/"),
|
||||
value=api_model.name,
|
||||
)
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
|
||||
for api_model in sorted(
|
||||
api_models, key=lambda x: x.name.lstrip("models/") or ""
|
||||
)
|
||||
if (
|
||||
api_model.display_name
|
||||
and api_model.name
|
||||
api_model.name
|
||||
and ("tts" in api_model.name) == (subentry_type == "tts")
|
||||
and "vision" not in api_model.name
|
||||
and api_model.supported_actions
|
||||
|
||||
@@ -11,6 +11,7 @@ from urllib.parse import quote
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import ClientTimeout, ClientWebSocketResponse, hdrs, web
|
||||
from aiohttp.helpers import must_be_empty_body
|
||||
from aiohttp.web_exceptions import HTTPBadGateway, HTTPBadRequest
|
||||
from multidict import CIMultiDict
|
||||
from yarl import URL
|
||||
@@ -184,13 +185,16 @@ class HassIOIngress(HomeAssistantView):
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
# Simple request
|
||||
if result.status in (204, 304) or (
|
||||
if (empty_body := must_be_empty_body(result.method, result.status)) or (
|
||||
content_length is not UNDEFINED
|
||||
and (content_length_int := int(content_length))
|
||||
<= MAX_SIMPLE_RESPONSE_SIZE
|
||||
):
|
||||
# Return Response
|
||||
body = await result.read()
|
||||
if empty_body:
|
||||
body = None
|
||||
else:
|
||||
body = await result.read()
|
||||
simple_response = web.Response(
|
||||
headers=headers,
|
||||
status=result.status,
|
||||
|
||||
@@ -63,8 +63,8 @@ from .utils import get_device_macs, non_verifying_requests_session
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Huawei LTE config flow."""
|
||||
class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Huawei LTE config flow."""
|
||||
|
||||
VERSION = 3
|
||||
|
||||
@@ -75,9 +75,9 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
) -> HuaweiLteOptionsFlow:
|
||||
"""Get options flow."""
|
||||
return OptionsFlowHandler()
|
||||
return HuaweiLteOptionsFlow()
|
||||
|
||||
async def _async_show_user_form(
|
||||
self,
|
||||
@@ -354,7 +354,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_update_reload_and_abort(entry, data=new_data)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
class HuaweiLteOptionsFlow(OptionsFlow):
|
||||
"""Huawei LTE options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -73,7 +73,6 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
|
||||
schedule = self.mower_attributes.calendar
|
||||
cursor = schedule.timeline.active_after(dt_util.now())
|
||||
program_event = next(cursor, None)
|
||||
_LOGGER.debug("program_event %s", program_event)
|
||||
if not program_event:
|
||||
return None
|
||||
work_area_name = None
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==11.2.1"]
|
||||
"requirements": ["Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -209,6 +209,12 @@
|
||||
"state": {
|
||||
"off": "mdi:card-bulleted-off-outline"
|
||||
}
|
||||
},
|
||||
"boost": {
|
||||
"default": "mdi:thermometer-high",
|
||||
"state": {
|
||||
"off": "mdi:thermometer-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +464,16 @@ class IronOSTemperatureNumberEntity(IronOSNumberEntity):
|
||||
else super().native_max_value
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
if (
|
||||
self.entity_description.key is PinecilNumber.BOOST_TEMP
|
||||
and self.native_value == 0
|
||||
):
|
||||
return False
|
||||
return super().available
|
||||
|
||||
|
||||
class IronOSSetpointNumberEntity(IronOSTemperatureNumberEntity):
|
||||
"""IronOS setpoint temperature entity."""
|
||||
|
||||
@@ -278,6 +278,9 @@
|
||||
},
|
||||
"calibrate_cjc": {
|
||||
"name": "Calibrate CJC"
|
||||
},
|
||||
"boost": {
|
||||
"name": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pynecil import CharSetting, SettingsDataResponse
|
||||
from pynecil import CharSetting, SettingsDataResponse, TempUnit
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import IronOSConfigEntry
|
||||
from .const import MIN_BOOST_TEMP, MIN_BOOST_TEMP_F
|
||||
from .coordinator import IronOSCoordinators
|
||||
from .entity import IronOSBaseEntity
|
||||
|
||||
@@ -39,6 +40,7 @@ class IronOSSwitch(StrEnum):
|
||||
INVERT_BUTTONS = "invert_buttons"
|
||||
DISPLAY_INVERT = "display_invert"
|
||||
CALIBRATE_CJC = "calibrate_cjc"
|
||||
BOOST = "boost"
|
||||
|
||||
|
||||
SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
@@ -94,6 +96,13 @@ SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
IronOSSwitchEntityDescription(
|
||||
key=IronOSSwitch.BOOST,
|
||||
translation_key=IronOSSwitch.BOOST,
|
||||
characteristic=CharSetting.BOOST_TEMP,
|
||||
is_on_fn=lambda x: bool(x.get("boost_temp")),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -136,7 +145,15 @@ class IronOSSwitchEntity(IronOSBaseEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
if self.entity_description.key is IronOSSwitch.BOOST:
|
||||
await self.settings.write(
|
||||
self.entity_description.characteristic,
|
||||
MIN_BOOST_TEMP_F
|
||||
if self.settings.data.get("temp_unit") is TempUnit.FAHRENHEIT
|
||||
else MIN_BOOST_TEMP,
|
||||
)
|
||||
else:
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
|
||||
@@ -39,7 +39,8 @@ from .const import (
|
||||
KNX_MODULE_KEY,
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SENSOR
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -146,17 +147,17 @@ class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxBinarySensor(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address_state=[
|
||||
config[DOMAIN][CONF_GA_SENSOR][CONF_GA_STATE],
|
||||
*config[DOMAIN][CONF_GA_SENSOR][CONF_GA_PASSIVE],
|
||||
],
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN].get(CONF_INVERT, False),
|
||||
ignore_internal_state=config[DOMAIN].get(CONF_IGNORE_INTERNAL_STATE, False),
|
||||
context_timeout=config[DOMAIN].get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=config[DOMAIN].get(CONF_RESET_AFTER),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SENSOR),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
invert=knx_conf.get(CONF_INVERT, default=False),
|
||||
ignore_internal_state=knx_conf.get(
|
||||
CONF_IGNORE_INTERNAL_STATE, default=False
|
||||
),
|
||||
context_timeout=knx_conf.get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=knx_conf.get(CONF_RESET_AFTER),
|
||||
)
|
||||
self._attr_force_update = self._device.ignore_internal_state
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import Cover as XknxCover
|
||||
@@ -35,15 +35,13 @@ from .schema import CoverSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_ANGLE,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_POSITION_SET,
|
||||
CONF_GA_POSITION_STATE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_STEP,
|
||||
CONF_GA_STOP,
|
||||
CONF_GA_UP_DOWN,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -230,38 +228,24 @@ class KnxYamlCover(_KnxCover, KnxYamlEntity):
|
||||
def _create_ui_cover(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxCover:
|
||||
"""Return a KNX Light device to be used within XKNX."""
|
||||
|
||||
def get_address(
|
||||
key: str, address_type: Literal["write", "state"] = CONF_GA_WRITE
|
||||
) -> str | None:
|
||||
"""Get a single group address for given key."""
|
||||
return knx_config[key][address_type] if key in knx_config else None
|
||||
|
||||
def get_addresses(
|
||||
key: str, address_type: Literal["write", "state"] = CONF_GA_STATE
|
||||
) -> list[Any] | None:
|
||||
"""Get group address including passive addresses as list."""
|
||||
return (
|
||||
[knx_config[key][address_type], *knx_config[key][CONF_GA_PASSIVE]]
|
||||
if key in knx_config
|
||||
else None
|
||||
)
|
||||
conf = ConfigExtractor(knx_config)
|
||||
|
||||
return XknxCover(
|
||||
xknx=xknx,
|
||||
name=name,
|
||||
group_address_long=get_addresses(CONF_GA_UP_DOWN, CONF_GA_WRITE),
|
||||
group_address_short=get_addresses(CONF_GA_STEP, CONF_GA_WRITE),
|
||||
group_address_stop=get_addresses(CONF_GA_STOP, CONF_GA_WRITE),
|
||||
group_address_position=get_addresses(CONF_GA_POSITION_SET, CONF_GA_WRITE),
|
||||
group_address_position_state=get_addresses(CONF_GA_POSITION_STATE),
|
||||
group_address_angle=get_address(CONF_GA_ANGLE),
|
||||
group_address_angle_state=get_addresses(CONF_GA_ANGLE),
|
||||
travel_time_down=knx_config[CoverConf.TRAVELLING_TIME_DOWN],
|
||||
travel_time_up=knx_config[CoverConf.TRAVELLING_TIME_UP],
|
||||
invert_updown=knx_config.get(CoverConf.INVERT_UPDOWN, False),
|
||||
invert_position=knx_config.get(CoverConf.INVERT_POSITION, False),
|
||||
invert_angle=knx_config.get(CoverConf.INVERT_ANGLE, False),
|
||||
sync_state=knx_config[CONF_SYNC_STATE],
|
||||
group_address_long=conf.get_write_and_passive(CONF_GA_UP_DOWN),
|
||||
group_address_short=conf.get_write_and_passive(CONF_GA_STEP),
|
||||
group_address_stop=conf.get_write_and_passive(CONF_GA_STOP),
|
||||
group_address_position=conf.get_write_and_passive(CONF_GA_POSITION_SET),
|
||||
group_address_position_state=conf.get_state_and_passive(CONF_GA_POSITION_STATE),
|
||||
group_address_angle=conf.get_write(CONF_GA_ANGLE),
|
||||
group_address_angle_state=conf.get_state_and_passive(CONF_GA_ANGLE),
|
||||
travel_time_down=conf.get(CoverConf.TRAVELLING_TIME_DOWN),
|
||||
travel_time_up=conf.get(CoverConf.TRAVELLING_TIME_UP),
|
||||
invert_updown=conf.get(CoverConf.INVERT_UPDOWN, default=False),
|
||||
invert_position=conf.get(CoverConf.INVERT_POSITION, default=False),
|
||||
invert_angle=conf.get(CoverConf.INVERT_ANGLE, default=False),
|
||||
sync_state=conf.get(CONF_SYNC_STATE),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ from .schema import LightSchema
|
||||
from .storage.const import (
|
||||
CONF_COLOR_TEMP_MAX,
|
||||
CONF_COLOR_TEMP_MIN,
|
||||
CONF_DPT,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_BLUE_BRIGHTNESS,
|
||||
CONF_GA_BLUE_SWITCH,
|
||||
@@ -45,17 +44,15 @@ from .storage.const import (
|
||||
CONF_GA_GREEN_BRIGHTNESS,
|
||||
CONF_GA_GREEN_SWITCH,
|
||||
CONF_GA_HUE,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_RED_BRIGHTNESS,
|
||||
CONF_GA_RED_SWITCH,
|
||||
CONF_GA_SATURATION,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WHITE_BRIGHTNESS,
|
||||
CONF_GA_WHITE_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.entity_store_schema import LightColorMode
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -203,94 +200,92 @@ def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight:
|
||||
def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight:
|
||||
"""Return a KNX Light device to be used within XKNX."""
|
||||
|
||||
def get_write(key: str) -> str | None:
|
||||
"""Get the write group address."""
|
||||
return knx_config[key][CONF_GA_WRITE] if key in knx_config else None
|
||||
|
||||
def get_state(key: str) -> list[Any] | None:
|
||||
"""Get the state group address."""
|
||||
return (
|
||||
[knx_config[key][CONF_GA_STATE], *knx_config[key][CONF_GA_PASSIVE]]
|
||||
if key in knx_config
|
||||
else None
|
||||
)
|
||||
|
||||
def get_dpt(key: str) -> str | None:
|
||||
"""Get the DPT."""
|
||||
return knx_config[key].get(CONF_DPT) if key in knx_config else None
|
||||
conf = ConfigExtractor(knx_config)
|
||||
|
||||
group_address_tunable_white = None
|
||||
group_address_tunable_white_state = None
|
||||
group_address_color_temp = None
|
||||
group_address_color_temp_state = None
|
||||
|
||||
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
|
||||
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_tunable_white_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if _color_temp_dpt := conf.get_dpt(CONF_GA_COLOR_TEMP):
|
||||
if _color_temp_dpt == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = conf.get_write(CONF_GA_COLOR_TEMP)
|
||||
group_address_tunable_white_state = conf.get_state_and_passive(
|
||||
CONF_GA_COLOR_TEMP
|
||||
)
|
||||
else:
|
||||
# absolute uint or float
|
||||
group_address_color_temp = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_color_temp_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
group_address_color_temp = conf.get_write(CONF_GA_COLOR_TEMP)
|
||||
group_address_color_temp_state = conf.get_state_and_passive(
|
||||
CONF_GA_COLOR_TEMP
|
||||
)
|
||||
if _color_temp_dpt == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
|
||||
|
||||
_color_dpt = get_dpt(CONF_GA_COLOR)
|
||||
color_dpt = conf.get_dpt(CONF_GA_COLOR)
|
||||
|
||||
return XknxLight(
|
||||
xknx,
|
||||
name=name,
|
||||
group_address_switch=get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=get_state(CONF_GA_SWITCH),
|
||||
group_address_brightness=get_write(CONF_GA_BRIGHTNESS),
|
||||
group_address_brightness_state=get_state(CONF_GA_BRIGHTNESS),
|
||||
group_address_color=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
group_address_switch=conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
group_address_brightness=conf.get_write(CONF_GA_BRIGHTNESS),
|
||||
group_address_brightness_state=conf.get_state_and_passive(CONF_GA_BRIGHTNESS),
|
||||
group_address_color=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_color_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
group_address_color_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_rgbw=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_rgbw_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_hue=get_write(CONF_GA_HUE),
|
||||
group_address_hue_state=get_state(CONF_GA_HUE),
|
||||
group_address_saturation=get_write(CONF_GA_SATURATION),
|
||||
group_address_saturation_state=get_state(CONF_GA_SATURATION),
|
||||
group_address_xyy_color=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.XYY
|
||||
group_address_hue=conf.get_write(CONF_GA_HUE),
|
||||
group_address_hue_state=conf.get_state_and_passive(CONF_GA_HUE),
|
||||
group_address_saturation=conf.get_write(CONF_GA_SATURATION),
|
||||
group_address_saturation_state=conf.get_state_and_passive(CONF_GA_SATURATION),
|
||||
group_address_xyy_color=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
else None,
|
||||
group_address_xyy_color_state=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.XYY
|
||||
group_address_xyy_color_state=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
else None,
|
||||
group_address_tunable_white=group_address_tunable_white,
|
||||
group_address_tunable_white_state=group_address_tunable_white_state,
|
||||
group_address_color_temperature=group_address_color_temp,
|
||||
group_address_color_temperature_state=group_address_color_temp_state,
|
||||
group_address_switch_red=get_write(CONF_GA_RED_SWITCH),
|
||||
group_address_switch_red_state=get_state(CONF_GA_RED_SWITCH),
|
||||
group_address_brightness_red=get_write(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_brightness_red_state=get_state(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_switch_green=get_write(CONF_GA_GREEN_SWITCH),
|
||||
group_address_switch_green_state=get_state(CONF_GA_GREEN_SWITCH),
|
||||
group_address_brightness_green=get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green_state=get_state(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_switch_blue=get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=get_state(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_brightness_blue_state=get_state(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_switch_white=get_write(CONF_GA_WHITE_SWITCH),
|
||||
group_address_switch_white_state=get_state(CONF_GA_WHITE_SWITCH),
|
||||
group_address_brightness_white=get_write(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_brightness_white_state=get_state(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_switch_red=conf.get_write(CONF_GA_RED_SWITCH),
|
||||
group_address_switch_red_state=conf.get_state_and_passive(CONF_GA_RED_SWITCH),
|
||||
group_address_brightness_red=conf.get_write(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_brightness_red_state=conf.get_state_and_passive(
|
||||
CONF_GA_RED_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_green=conf.get_write(CONF_GA_GREEN_SWITCH),
|
||||
group_address_switch_green_state=conf.get_state_and_passive(
|
||||
CONF_GA_GREEN_SWITCH
|
||||
),
|
||||
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green_state=conf.get_state_and_passive(
|
||||
CONF_GA_GREEN_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_brightness_blue_state=conf.get_state_and_passive(
|
||||
CONF_GA_BLUE_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_white=conf.get_write(CONF_GA_WHITE_SWITCH),
|
||||
group_address_switch_white_state=conf.get_state_and_passive(
|
||||
CONF_GA_WHITE_SWITCH
|
||||
),
|
||||
group_address_brightness_white=conf.get_write(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_brightness_white_state=conf.get_state_and_passive(
|
||||
CONF_GA_WHITE_BRIGHTNESS
|
||||
),
|
||||
color_temperature_type=color_temperature_type,
|
||||
min_kelvin=knx_config[CONF_COLOR_TEMP_MIN],
|
||||
max_kelvin=knx_config[CONF_COLOR_TEMP_MAX],
|
||||
|
||||
51
homeassistant/components/knx/storage/util.py
Normal file
51
homeassistant/components/knx/storage/util.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Utility functions for the KNX integration."""
|
||||
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE
|
||||
|
||||
|
||||
def nested_get(dic: ConfigType, *keys: str, default: Any | None = None) -> Any:
|
||||
"""Get the value from a nested dictionary."""
|
||||
for key in keys:
|
||||
if key not in dic:
|
||||
return default
|
||||
dic = dic[key]
|
||||
return dic
|
||||
|
||||
|
||||
class ConfigExtractor:
|
||||
"""Helper class for extracting values from a knx config store dictionary."""
|
||||
|
||||
__slots__ = ("get",)
|
||||
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the extractor."""
|
||||
self.get = partial(nested_get, config)
|
||||
|
||||
def get_write(self, *path: str) -> str | None:
|
||||
"""Get the write group address."""
|
||||
return self.get(*path, CONF_GA_WRITE) # type: ignore[no-any-return]
|
||||
|
||||
def get_state(self, *path: str) -> str | None:
|
||||
"""Get the state group address."""
|
||||
return self.get(*path, CONF_GA_STATE) # type: ignore[no-any-return]
|
||||
|
||||
def get_write_and_passive(self, *path: str) -> list[Any | None]:
|
||||
"""Get the group addresses of write and passive."""
|
||||
write = self.get(*path, CONF_GA_WRITE)
|
||||
passive = self.get(*path, CONF_GA_PASSIVE)
|
||||
return [write, *passive] if passive else [write]
|
||||
|
||||
def get_state_and_passive(self, *path: str) -> list[Any | None]:
|
||||
"""Get the group addresses of state and passive."""
|
||||
state = self.get(*path, CONF_GA_STATE)
|
||||
passive = self.get(*path, CONF_GA_PASSIVE)
|
||||
return [state, *passive] if passive else [state]
|
||||
|
||||
def get_dpt(self, *path: str) -> str | None:
|
||||
"""Get the data point type of a group address config key."""
|
||||
return self.get(*path, CONF_DPT) # type: ignore[no-any-return]
|
||||
@@ -36,13 +36,8 @@ from .const import (
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .schema import SwitchSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SWITCH
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -142,15 +137,13 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxSwitch(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE],
|
||||
group_address_state=[
|
||||
config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE],
|
||||
*config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE],
|
||||
],
|
||||
respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ],
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN][CONF_INVERT],
|
||||
group_address=knx_conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
invert=knx_conf.get(CONF_INVERT),
|
||||
)
|
||||
|
||||
@@ -104,7 +104,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -
|
||||
) as ex:
|
||||
await lcn_connection.async_close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to {config_entry.title}: {ex}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"config_entry_title": config_entry.title,
|
||||
},
|
||||
) from ex
|
||||
|
||||
_LOGGER.info('LCN connected to "%s"', config_entry.title)
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
CONF_SWITCHES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -100,7 +101,11 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str:
|
||||
return cast(str, domain_data["setpoint"])
|
||||
if domain_name == "scene":
|
||||
return f"{domain_data['register']}{domain_data['scene']}"
|
||||
raise ValueError("Unknown domain")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_domain",
|
||||
translation_placeholders={CONF_DOMAIN: domain_name},
|
||||
)
|
||||
|
||||
|
||||
def generate_unique_id(
|
||||
@@ -304,6 +309,8 @@ def get_device_config(
|
||||
def is_states_string(states_string: str) -> list[str]:
|
||||
"""Validate the given states string and return states list."""
|
||||
if len(states_string) != 8:
|
||||
raise ValueError("Invalid length of states string")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_length_of_states_string"
|
||||
)
|
||||
states = {"1": "ON", "0": "OFF", "T": "TOGGLE", "-": "NOCHANGE"}
|
||||
return [states[state_string] for state_string in states_string]
|
||||
|
||||
@@ -19,7 +19,7 @@ rules:
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
|
||||
@@ -330,8 +330,9 @@ class SendKeys(LcnServiceCall):
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
hit = pypck.lcn_defs.SendKeyCommand.HIT
|
||||
if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit:
|
||||
raise ValueError(
|
||||
"Only hit command is allowed when sending deferred keys."
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_send_keys_action",
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit)
|
||||
@@ -368,8 +369,9 @@ class LockKeys(LcnServiceCall):
|
||||
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
if table_id != 0:
|
||||
raise ValueError(
|
||||
"Only table A is allowed when locking keys for a specific time."
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_lock_keys_table",
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.lock_keys_tab_a_temporary(
|
||||
|
||||
@@ -414,11 +414,23 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_address": {
|
||||
"message": "LCN device for given address has not been configured."
|
||||
"cannot_connect": {
|
||||
"message": "Unable to connect to {config_entry_title}."
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "LCN device for given device ID has not been configured."
|
||||
"message": "LCN device for given device ID {device_id} has not been configured."
|
||||
},
|
||||
"invalid_domain": {
|
||||
"message": "Invalid domain {domain}."
|
||||
},
|
||||
"invalid_send_keys_action": {
|
||||
"message": "Invalid state for sending keys. Only 'hit' allowed for deferred sending."
|
||||
},
|
||||
"invalid_lock_keys_table": {
|
||||
"message": "Invalid table for locking keys. Only table A allowed when locking for a specific time."
|
||||
},
|
||||
"invalid_length_of_states_string": {
|
||||
"message": "Invalid length of states string. Expected 8 characters."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "led-ble==1.1.7"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, entry: LookinConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove lookin config entry from a device."""
|
||||
data: LookinData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
all_identifiers: set[tuple[str, str]] = {
|
||||
(DOMAIN, data.lookin_device.id),
|
||||
*((DOMAIN, remote["UUID"]) for remote in data.devices),
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.2.1"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==7.0.0"],
|
||||
"requirements": ["python-matter-server==8.0.0"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, cast
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
|
||||
from matter_server.client.models import device_types
|
||||
from matter_server.common import custom_clusters
|
||||
|
||||
from homeassistant.components.number import (
|
||||
@@ -18,6 +19,7 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfLength,
|
||||
@@ -123,6 +125,31 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
)
|
||||
|
||||
|
||||
class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
"""Representation of a Matter Attribute as a Number entity."""
|
||||
|
||||
entity_description: MatterNumberEntityDescription
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set level value."""
|
||||
send_value = int(value)
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
send_value = value_convert(value)
|
||||
await self.send_device_command(
|
||||
clusters.LevelControl.Commands.MoveToLevel(
|
||||
level=send_value,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
@@ -239,6 +266,26 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
vendor_id=(4874,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="pump_setpoint",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
translation_key="pump_setpoint",
|
||||
native_max_value=100,
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
measurement_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
),
|
||||
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
entity_class=MatterLevelControlNumber,
|
||||
required_attributes=(clusters.LevelControl.Attributes.CurrentLevel,),
|
||||
device_type=(device_types.Pump,),
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
|
||||
@@ -180,6 +180,9 @@
|
||||
"altitude": {
|
||||
"name": "Altitude above sea level"
|
||||
},
|
||||
"pump_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
|
||||
@@ -32,11 +32,18 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
llm_apis = {api.id: api.name for api in llm.async_get_apis(self.hass)}
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input
|
||||
)
|
||||
if not user_input[CONF_LLM_HASS_API]:
|
||||
errors[CONF_LLM_HASS_API] = "llm_api_required"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=", ".join(
|
||||
llm_apis[api_id] for api_id in user_input[CONF_LLM_HASS_API]
|
||||
),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -44,7 +51,7 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
default=llm.LLM_API_ASSIST,
|
||||
default=[llm.LLM_API_ASSIST],
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
@@ -53,10 +60,12 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
value=llm_api_id,
|
||||
)
|
||||
for llm_api_id, name in llm_apis.items()
|
||||
]
|
||||
],
|
||||
multiple=True,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders={"more_info_url": MORE_INFO_URL},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ def _format_tool(
|
||||
|
||||
|
||||
async def create_server(
|
||||
hass: HomeAssistant, llm_api_id: str, llm_context: llm.LLMContext
|
||||
hass: HomeAssistant, llm_api_id: str | list[str], llm_context: llm.LLMContext
|
||||
) -> Server:
|
||||
"""Create a new Model Context Protocol Server.
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"llm_api_required": "At least one LLM API must be configured."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
|
||||
@@ -2,34 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from medcom_ble import MedcomBleDeviceData
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
|
||||
# Supported platforms
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Medcom BLE radiation monitor from a config entry."""
|
||||
|
||||
address = entry.unique_id
|
||||
elevation = hass.config.elevation
|
||||
is_metric = hass.config.units is METRIC_SYSTEM
|
||||
assert address is not None
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
@@ -38,26 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"Could not find Medcom BLE device with address {address}"
|
||||
)
|
||||
|
||||
async def _async_update_method():
|
||||
"""Get data from Medcom BLE radiation monitor."""
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
inspector = MedcomBleDeviceData(_LOGGER, elevation, is_metric)
|
||||
|
||||
try:
|
||||
data = await inspector.update_device(ble_device)
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_method,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
coordinator = MedcomBleUpdateCoordinator(hass, entry, address)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
50
homeassistant/components/medcom_ble/coordinator.py
Normal file
50
homeassistant/components/medcom_ble/coordinator.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""The Medcom BLE integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from medcom_ble import MedcomBleDevice, MedcomBleDeviceData
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]):
|
||||
"""Coordinator for Medcom BLE radiation monitor data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._address = address
|
||||
self._elevation = hass.config.elevation
|
||||
self._is_metric = hass.config.units is METRIC_SYSTEM
|
||||
|
||||
async def _async_update_data(self) -> MedcomBleDevice:
|
||||
"""Get data from Medcom BLE radiation monitor."""
|
||||
ble_device = bluetooth.async_ble_device_from_address(self.hass, self._address)
|
||||
inspector = MedcomBleDeviceData(_LOGGER, self._elevation, self._is_metric)
|
||||
|
||||
try:
|
||||
data = await inspector.update_device(ble_device)
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from medcom_ble import MedcomBleDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -15,12 +13,10 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, UNIT_CPM
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,9 +37,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Medcom BLE radiation monitor sensors."""
|
||||
|
||||
coordinator: DataUpdateCoordinator[MedcomBleDevice] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities = []
|
||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||
@@ -62,16 +56,14 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MedcomSensor(
|
||||
CoordinatorEntity[DataUpdateCoordinator[MedcomBleDevice]], SensorEntity
|
||||
):
|
||||
class MedcomSensor(CoordinatorEntity[MedcomBleUpdateCoordinator], SensorEntity):
|
||||
"""Medcom BLE radiation monitor sensors for the device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[MedcomBleDevice],
|
||||
coordinator: MedcomBleUpdateCoordinator,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Populate the medcom entity with relevant data."""
|
||||
|
||||
@@ -30,6 +30,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
MEDIA_CLASS_MAP,
|
||||
MEDIA_MIME_TYPES,
|
||||
MEDIA_SOURCE_DATA,
|
||||
URI_SCHEME,
|
||||
URI_SCHEME_REGEX,
|
||||
)
|
||||
@@ -78,7 +79,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the media_source component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[MEDIA_SOURCE_DATA] = {}
|
||||
websocket_api.async_register_command(hass, websocket_browse_media)
|
||||
websocket_api.async_register_command(hass, websocket_resolve_media)
|
||||
frontend.async_register_built_in_panel(
|
||||
@@ -97,7 +98,7 @@ async def _process_media_source_platform(
|
||||
platform: MediaSourceProtocol,
|
||||
) -> None:
|
||||
"""Process a media source platform."""
|
||||
hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass)
|
||||
hass.data[MEDIA_SOURCE_DATA][domain] = await platform.async_get_media_source(hass)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -109,10 +110,10 @@ def _get_media_item(
|
||||
item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player)
|
||||
else:
|
||||
# We default to our own domain if its only one registered
|
||||
domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN
|
||||
domain = None if len(hass.data[MEDIA_SOURCE_DATA]) > 1 else DOMAIN
|
||||
return MediaSourceItem(hass, domain, "", target_media_player)
|
||||
|
||||
if item.domain is not None and item.domain not in hass.data[DOMAIN]:
|
||||
if item.domain is not None and item.domain not in hass.data[MEDIA_SOURCE_DATA]:
|
||||
raise UnknownMediaSource(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unknown_media_source",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
"""Constants for the media_source integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.media_player import MediaClass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import MediaSource
|
||||
|
||||
DOMAIN = "media_source"
|
||||
MEDIA_SOURCE_DATA: HassKey[dict[str, MediaSource]] = HassKey(DOMAIN)
|
||||
MEDIA_MIME_TYPES = ("audio", "video", "image")
|
||||
MEDIA_CLASS_MAP = {
|
||||
"audio": MediaClass.MUSIC,
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_request import FileField
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, MEDIA_SOURCE_DATA
|
||||
from .error import Unresolvable
|
||||
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
||||
|
||||
@@ -30,7 +30,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up local media source."""
|
||||
source = LocalSource(hass)
|
||||
hass.data[DOMAIN][DOMAIN] = source
|
||||
hass.data[MEDIA_SOURCE_DATA][DOMAIN] = source
|
||||
hass.http.register_view(LocalMediaView(hass, source))
|
||||
hass.http.register_view(UploadMediaView(hass, source))
|
||||
websocket_api.async_register_command(hass, websocket_remove_media)
|
||||
@@ -352,7 +352,7 @@ async def websocket_remove_media(
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
return
|
||||
|
||||
source: LocalSource = hass.data[DOMAIN][DOMAIN]
|
||||
source = cast(LocalSource, hass.data[MEDIA_SOURCE_DATA][DOMAIN])
|
||||
|
||||
try:
|
||||
source_dir_id, location = source.async_parse_identifier(item)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
|
||||
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -70,7 +70,7 @@ class MediaSourceItem:
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for source in self.hass.data[DOMAIN].values()
|
||||
for source in self.hass.data[MEDIA_SOURCE_DATA].values()
|
||||
),
|
||||
key=lambda item: item.title,
|
||||
)
|
||||
@@ -85,7 +85,9 @@ class MediaSourceItem:
|
||||
@callback
|
||||
def async_media_source(self) -> MediaSource:
|
||||
"""Return media source that owns this item."""
|
||||
return cast(MediaSource, self.hass.data[DOMAIN][self.domain])
|
||||
if TYPE_CHECKING:
|
||||
assert self.domain is not None
|
||||
return self.hass.data[MEDIA_SOURCE_DATA][self.domain]
|
||||
|
||||
@classmethod
|
||||
def from_uri(
|
||||
|
||||
@@ -27,9 +27,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
type MelCloudConfigEntry = ConfigEntry[dict[str, list[MelCloudDevice]]]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Establish connection with MELClooud."""
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) -> bool:
|
||||
"""Establish connection with MELCloud."""
|
||||
conf = entry.data
|
||||
try:
|
||||
mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN])
|
||||
@@ -40,20 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except (TimeoutError, ClientConnectionError) as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices})
|
||||
entry.runtime_data = mel_devices
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
class MelCloudDevice:
|
||||
|
||||
@@ -24,13 +24,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudDevice
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from .const import (
|
||||
ATTR_STATUS,
|
||||
ATTR_VANE_HORIZONTAL,
|
||||
@@ -38,7 +37,6 @@ from .const import (
|
||||
ATTR_VANE_VERTICAL,
|
||||
ATTR_VANE_VERTICAL_POSITIONS,
|
||||
CONF_POSITION,
|
||||
DOMAIN,
|
||||
SERVICE_SET_VANE_HORIZONTAL,
|
||||
SERVICE_SET_VANE_VERTICAL,
|
||||
)
|
||||
@@ -77,11 +75,11 @@ ATW_ZONE_HVAC_ACTION_LOOKUP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MelCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MelCloud device climate based on config_entry."""
|
||||
mel_devices = hass.data[DOMAIN][entry.entry_id]
|
||||
mel_devices = entry.runtime_data
|
||||
entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [
|
||||
AtaDeviceClimate(mel_device, mel_device.device)
|
||||
for mel_device in mel_devices[DEVICE_TYPE_ATA]
|
||||
|
||||
@@ -5,11 +5,12 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import MelCloudConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_USERNAME,
|
||||
CONF_TOKEN,
|
||||
@@ -17,7 +18,7 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: MelCloudConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for the config entry."""
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
@@ -15,13 +15,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudDevice
|
||||
from .const import DOMAIN
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
@@ -105,11 +103,11 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MelCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MELCloud device sensors based on config_entry."""
|
||||
mel_devices = hass.data[DOMAIN].get(entry.entry_id)
|
||||
mel_devices = entry.runtime_data
|
||||
|
||||
entities: list[MelDeviceSensor] = [
|
||||
MelDeviceSensor(mel_device, description)
|
||||
|
||||
@@ -17,22 +17,21 @@ from homeassistant.components.water_heater import (
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, MelCloudDevice
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from .const import ATTR_STATUS
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MelCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MelCloud device climate based on config_entry."""
|
||||
mel_devices = hass.data[DOMAIN][entry.entry_id]
|
||||
mel_devices = entry.runtime_data
|
||||
async_add_entities(
|
||||
[
|
||||
AtwWaterHeater(mel_device, mel_device.device)
|
||||
|
||||
@@ -6,13 +6,11 @@ from melnor_bluetooth.device import Device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
@@ -22,11 +20,8 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
|
||||
"""Set up melnor from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS])
|
||||
|
||||
if not ble_device:
|
||||
@@ -60,20 +55,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = MelnorDataUpdateCoordinator(hass, entry, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.data.disconnect()
|
||||
|
||||
device: Device = hass.data[DOMAIN][entry.entry_id].data
|
||||
|
||||
await device.disconnect()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -11,15 +11,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MelnorConfigEntry = ConfigEntry[MelnorDataUpdateCoordinator]
|
||||
|
||||
|
||||
class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
"""Melnor data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MelnorConfigEntry
|
||||
_device: Device
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
|
||||
self, hass: HomeAssistant, config_entry: MelnorConfigEntry, device: Device
|
||||
) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -13,13 +13,11 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -67,12 +65,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the number platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -26,8 +25,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorBluetoothEntity, MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -104,12 +102,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
# Device-level sensors
|
||||
async_add_entities(
|
||||
|
||||
@@ -13,12 +13,10 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -51,12 +49,12 @@ ZONE_ENTITY_DESCRIPTIONS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
||||
@@ -10,13 +10,11 @@ from typing import Any
|
||||
from melnor_bluetooth.device import Valve
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -41,12 +39,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneTimeEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MelnorConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the number platform."""
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
||||
@@ -1,59 +1,21 @@
|
||||
"""The met_eireann component."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Self
|
||||
|
||||
import meteireann
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||
from .coordinator import MetEireannUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.WEATHER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Met Éireann as config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
raw_weather_data = meteireann.WeatherData(
|
||||
async_get_clientsession(hass),
|
||||
latitude=config_entry.data[CONF_LATITUDE],
|
||||
longitude=config_entry.data[CONF_LONGITUDE],
|
||||
altitude=config_entry.data[CONF_ELEVATION],
|
||||
)
|
||||
|
||||
weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
|
||||
|
||||
async def _async_update_data() -> MetEireannWeatherData:
|
||||
"""Fetch data from Met Éireann."""
|
||||
try:
|
||||
return await weather_data.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_data,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
coordinator = MetEireannUpdateCoordinator(hass, config_entry=config_entry)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@@ -68,26 +30,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class MetEireannWeatherData:
|
||||
"""Keep data for Met Éireann weather entities."""
|
||||
|
||||
def __init__(
|
||||
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
|
||||
) -> None:
|
||||
"""Initialise the weather entity data."""
|
||||
self._config = config
|
||||
self._weather_data = weather_data
|
||||
self.current_weather_data: dict[str, Any] = {}
|
||||
self.daily_forecast: list[dict[str, Any]] = []
|
||||
self.hourly_forecast: list[dict[str, Any]] = []
|
||||
|
||||
async def fetch_data(self) -> Self:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
||||
76
homeassistant/components/met_eireann/coordinator.py
Normal file
76
homeassistant/components/met_eireann/coordinator.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""The met_eireann component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Self
|
||||
|
||||
import meteireann
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||
|
||||
|
||||
class MetEireannWeatherData:
|
||||
"""Keep data for Met Éireann weather entities."""
|
||||
|
||||
def __init__(
|
||||
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
|
||||
) -> None:
|
||||
"""Initialise the weather entity data."""
|
||||
self._config = config
|
||||
self._weather_data = weather_data
|
||||
self.current_weather_data: dict[str, Any] = {}
|
||||
self.daily_forecast: list[dict[str, Any]] = []
|
||||
self.hourly_forecast: list[dict[str, Any]] = []
|
||||
|
||||
async def fetch_data(self) -> Self:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
||||
|
||||
class MetEireannUpdateCoordinator(DataUpdateCoordinator[MetEireannWeatherData]):
|
||||
"""Coordinator for Met Éireann weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
raw_weather_data = meteireann.WeatherData(
|
||||
async_get_clientsession(hass),
|
||||
latitude=config_entry.data[CONF_LATITUDE],
|
||||
longitude=config_entry.data[CONF_LONGITUDE],
|
||||
altitude=config_entry.data[CONF_ELEVATION],
|
||||
)
|
||||
self._weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
|
||||
|
||||
async def _async_update_data(self) -> MetEireannWeatherData:
|
||||
"""Fetch data from Met Éireann."""
|
||||
try:
|
||||
return await self._weather_data.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for Met Éireann weather service."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
@@ -29,10 +28,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MetEireannWeatherData
|
||||
from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .coordinator import MetEireannWeatherData
|
||||
|
||||
|
||||
def format_condition(condition: str | None) -> str | None:
|
||||
|
||||
@@ -23,7 +23,6 @@ from .const import (
|
||||
COORDINATOR_RAIN,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -130,10 +129,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.title,
|
||||
)
|
||||
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
COORDINATOR_FORECAST: coordinator_forecast,
|
||||
}
|
||||
if coordinator_rain and coordinator_rain.last_update_success:
|
||||
@@ -163,7 +161,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
@@ -26,7 +26,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
COORDINATOR_FORECAST = "coordinator_forecast"
|
||||
COORDINATOR_RAIN = "coordinator_rain"
|
||||
COORDINATOR_ALERT = "coordinator_alert"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
ATTRIBUTION = "Data provided by Météo-France"
|
||||
MODEL = "Météo-France mobile API"
|
||||
MANUFACTURER = "Météo-France"
|
||||
|
||||
@@ -6,6 +6,8 @@ import time
|
||||
from meteofrance_api.model.forecast import Forecast as MeteoFranceForecast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_HUMIDITY,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||
@@ -49,9 +51,13 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_condition(condition: str):
|
||||
def format_condition(condition: str, force_day: bool = False) -> str:
|
||||
"""Return condition from dict CONDITION_MAP."""
|
||||
return CONDITION_MAP.get(condition, condition)
|
||||
mapped_condition = CONDITION_MAP.get(condition, condition)
|
||||
if force_day and mapped_condition == ATTR_CONDITION_CLEAR_NIGHT:
|
||||
# Meteo-France can return clear night condition instead of sunny for daily weather, so we map it to sunny
|
||||
return ATTR_CONDITION_SUNNY
|
||||
return mapped_condition
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -212,7 +218,7 @@ class MeteoFranceWeather(
|
||||
forecast["dt"]
|
||||
).isoformat(),
|
||||
ATTR_FORECAST_CONDITION: format_condition(
|
||||
forecast["weather12H"]["desc"]
|
||||
forecast["weather12H"]["desc"], force_day=True
|
||||
),
|
||||
ATTR_FORECAST_HUMIDITY: forecast["humidity"]["max"],
|
||||
ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"],
|
||||
|
||||
@@ -1,43 +1,15 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
import logging
|
||||
|
||||
from meteoclimatic import MeteoclimaticClient
|
||||
from meteoclimatic.exceptions import MeteoclimaticError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Meteoclimatic entry."""
|
||||
station_code = entry.data[CONF_STATION_CODE]
|
||||
meteoclimatic_client = MeteoclimaticClient()
|
||||
|
||||
async def async_update_data():
|
||||
"""Obtain the latest data from Meteoclimatic."""
|
||||
try:
|
||||
data = await hass.async_add_executor_job(
|
||||
meteoclimatic_client.weather_at_station, station_code
|
||||
)
|
||||
except MeteoclimaticError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
return data.__dict__
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meteoclimatic weather for {entry.title} ({station_code})",
|
||||
update_method=async_update_data,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
coordinator = MeteoclimaticUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
43
homeassistant/components/meteoclimatic/coordinator.py
Normal file
43
homeassistant/components/meteoclimatic/coordinator.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from meteoclimatic import MeteoclimaticClient
|
||||
from meteoclimatic.exceptions import MeteoclimaticError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_STATION_CODE, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for Meteoclimatic weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self._station_code = entry.data[CONF_STATION_CODE]
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meteoclimatic weather for {entry.title} ({self._station_code})",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self._meteoclimatic_client = MeteoclimaticClient()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Obtain the latest data from Meteoclimatic."""
|
||||
try:
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self._meteoclimatic_client.weather_at_station, self._station_code
|
||||
)
|
||||
except MeteoclimaticError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
return data.__dict__
|
||||
@@ -18,12 +18,10 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@@ -119,7 +117,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
[MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES],
|
||||
@@ -127,13 +125,17 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class MeteoclimaticSensor(CoordinatorEntity, SensorEntity):
|
||||
class MeteoclimaticSensor(
|
||||
CoordinatorEntity[MeteoclimaticUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Representation of a Meteoclimatic sensor."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
|
||||
def __init__(
|
||||
self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription
|
||||
self,
|
||||
coordinator: MeteoclimaticUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Meteoclimatic sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -8,12 +8,10 @@ from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
def format_condition(condition):
|
||||
@@ -31,12 +29,14 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic weather platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([MeteoclimaticWeather(coordinator)], False)
|
||||
|
||||
|
||||
class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
|
||||
class MeteoclimaticWeather(
|
||||
CoordinatorEntity[MeteoclimaticUpdateCoordinator], WeatherEntity
|
||||
):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
@@ -44,7 +44,7 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
|
||||
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
|
||||
def __init__(self, coordinator: MeteoclimaticUpdateCoordinator) -> None:
|
||||
"""Initialise the weather platform."""
|
||||
super().__init__(coordinator)
|
||||
self._unique_id = self.coordinator.data["station"].code
|
||||
|
||||
@@ -9,6 +9,7 @@ from datapoint.Forecast import Forecast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
EntityCategory,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
@@ -59,6 +60,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = (
|
||||
native_attr_name="name",
|
||||
name="Station name",
|
||||
icon="mdi:label-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
MetOfficeSensorEntityDescription(
|
||||
@@ -235,14 +237,13 @@ class MetOfficeCurrentSensor(
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
value = get_attribute(
|
||||
self.coordinator.data.now(), self.entity_description.native_attr_name
|
||||
)
|
||||
native_attr = self.entity_description.native_attr_name
|
||||
|
||||
if (
|
||||
self.entity_description.native_attr_name == "significantWeatherCode"
|
||||
and value is not None
|
||||
):
|
||||
if native_attr == "name":
|
||||
return str(self.coordinator.data.name)
|
||||
|
||||
value = get_attribute(self.coordinator.data.now(), native_attr)
|
||||
if native_attr == "significantWeatherCode" and value is not None:
|
||||
value = CONDITION_MAP.get(value)
|
||||
|
||||
return value
|
||||
|
||||
@@ -10,13 +10,7 @@ from homeassistant.const import CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import (
|
||||
CONF_NOT_FIRST_RUN,
|
||||
DOMAIN,
|
||||
FIRST_RUN,
|
||||
MONOPRICE_OBJECT,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from .const import CONF_NOT_FIRST_RUN, DOMAIN, FIRST_RUN, MONOPRICE_OBJECT
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
@@ -41,11 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry, data={**entry.data, CONF_NOT_FIRST_RUN: True}
|
||||
)
|
||||
|
||||
undo_listener = entry.add_update_listener(_update_listener)
|
||||
entry.async_on_unload(entry.add_update_listener(_update_listener))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
MONOPRICE_OBJECT: monoprice,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
FIRST_RUN: first_run,
|
||||
}
|
||||
|
||||
@@ -60,8 +53,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not unload_ok:
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
def _cleanup(monoprice) -> None:
|
||||
"""Destroy the Monoprice object.
|
||||
|
||||
|
||||
@@ -18,4 +18,3 @@ SERVICE_RESTORE = "restore"
|
||||
|
||||
FIRST_RUN = "first_run"
|
||||
MONOPRICE_OBJECT = "monoprice_object"
|
||||
UNDO_UPDATE_LISTENER = "update_update_listener"
|
||||
|
||||
@@ -34,7 +34,7 @@ class MusicAssistantEntity(Entity):
|
||||
identifiers={(DOMAIN, player_id)},
|
||||
manufacturer=self.player.device_info.manufacturer or provider.name,
|
||||
model=self.player.device_info.model or self.player.name,
|
||||
name=self.player.display_name,
|
||||
name=self.player.name,
|
||||
configuration_url=f"{mass.server_url}/#/settings/editplayer/{player_id}",
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["music_assistant"],
|
||||
"requirements": ["music-assistant-client==1.2.0"],
|
||||
"requirements": ["music-assistant-client==1.2.4"],
|
||||
"zeroconf": ["_mass._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from music_assistant_models.enums import MediaType as MASSMediaType
|
||||
from music_assistant_models.media_items import (
|
||||
BrowseFolder,
|
||||
MediaItemType,
|
||||
SearchResults,
|
||||
)
|
||||
from music_assistant_models.media_items import MediaItemType, SearchResults
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -549,8 +545,6 @@ def _process_search_results(
|
||||
|
||||
# Add available items to results
|
||||
for item in items:
|
||||
if TYPE_CHECKING:
|
||||
assert not isinstance(item, BrowseFolder)
|
||||
if not item.available:
|
||||
continue
|
||||
|
||||
|
||||
@@ -248,10 +248,8 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
player = self.player
|
||||
active_queue = self.active_queue
|
||||
# update generic attributes
|
||||
if player.powered and active_queue is not None:
|
||||
self._attr_state = MediaPlayerState(active_queue.state.value)
|
||||
if player.powered and player.state is not None:
|
||||
self._attr_state = MediaPlayerState(player.state.value)
|
||||
if player.powered and player.playback_state is not None:
|
||||
self._attr_state = MediaPlayerState(player.playback_state.value)
|
||||
else:
|
||||
self._attr_state = MediaPlayerState(STATE_OFF)
|
||||
# active source and source list (translate to HA source names)
|
||||
@@ -270,12 +268,12 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
self._attr_source = active_source_name
|
||||
|
||||
group_members: list[str] = []
|
||||
if player.group_childs:
|
||||
group_members = player.group_childs
|
||||
if player.group_members:
|
||||
group_members = player.group_members
|
||||
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
|
||||
group_members = parent.group_childs
|
||||
group_members = parent.group_members
|
||||
|
||||
# translate MA group_childs to HA group_members as entity id's
|
||||
# translate MA group_members to HA group_members as entity id's
|
||||
entity_registry = er.async_get(self.hass)
|
||||
group_members_entity_ids: list[str] = [
|
||||
entity_id
|
||||
|
||||
@@ -9,13 +9,11 @@ from pymystrom.bulb import MyStromBulb
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
from pymystrom.switch import MyStromSwitch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import MyStromData
|
||||
from .models import MyStromConfigEntry, MyStromData
|
||||
|
||||
PLATFORMS_PLUGS = [Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS_BULB = [Platform.LIGHT]
|
||||
@@ -41,7 +39,7 @@ def _get_mystrom_switch(host: str) -> MyStromSwitch:
|
||||
return MyStromSwitch(host)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool:
|
||||
"""Set up myStrom from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
try:
|
||||
@@ -73,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.error("Unsupported myStrom device type: %s", device_type)
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData(
|
||||
entry.runtime_data = MyStromData(
|
||||
device=device,
|
||||
info=info,
|
||||
)
|
||||
@@ -82,15 +80,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
device_type = hass.data[DOMAIN][entry.entry_id].info["type"]
|
||||
device_type = entry.runtime_data.info["type"]
|
||||
platforms = []
|
||||
if device_type in [101, 106, 107, 120]:
|
||||
platforms.extend(PLATFORMS_PLUGS)
|
||||
elif device_type in [102, 105]:
|
||||
platforms.extend(PLATFORMS_BULB)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, platforms)
|
||||
|
||||
@@ -15,12 +15,12 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .models import MyStromConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,12 +32,12 @@ EFFECT_SUNRISE = "sunrise"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MyStromConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the myStrom entities."""
|
||||
info = hass.data[DOMAIN][entry.entry_id].info
|
||||
device = hass.data[DOMAIN][entry.entry_id].device
|
||||
info = entry.runtime_data.info
|
||||
device = entry.runtime_data.device
|
||||
async_add_entities([MyStromLight(device, entry.title, info["mac"])])
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ from typing import Any
|
||||
from pymystrom.bulb import MyStromBulb
|
||||
from pymystrom.switch import MyStromSwitch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
type MyStromConfigEntry = ConfigEntry[MyStromData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MyStromData:
|
||||
|
||||
@@ -13,13 +13,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfPower, UnitOfTemperature
|
||||
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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -56,11 +56,11 @@ SENSOR_TYPES: tuple[MyStromSwitchSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MyStromConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the myStrom entities."""
|
||||
device: MyStromSwitch = hass.data[DOMAIN][entry.entry_id].device
|
||||
device: MyStromSwitch = entry.runtime_data.device
|
||||
|
||||
async_add_entities(
|
||||
MyStromSwitchSensor(device, entry.title, description)
|
||||
|
||||
@@ -8,12 +8,12 @@ from typing import Any
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .models import MyStromConfigEntry
|
||||
|
||||
DEFAULT_NAME = "myStrom Switch"
|
||||
|
||||
@@ -22,11 +22,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MyStromConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the myStrom entities."""
|
||||
device = hass.data[DOMAIN][entry.entry_id].device
|
||||
device = entry.runtime_data.device
|
||||
async_add_entities([MyStromSwitch(device, entry.title)])
|
||||
|
||||
|
||||
|
||||
@@ -44,15 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
|
||||
translation_key="device_communication_error",
|
||||
translation_placeholders={"device": entry.title},
|
||||
) from err
|
||||
|
||||
try:
|
||||
await nam.async_check_credentials()
|
||||
except (ApiError, ClientError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_communication_error",
|
||||
translation_placeholders={"device": entry.title},
|
||||
) from err
|
||||
except AuthFailedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -26,15 +25,6 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclass
|
||||
class NamConfig:
|
||||
"""NAM device configuration class."""
|
||||
|
||||
mac_address: str
|
||||
auth_enabled: bool
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTH_SCHEMA = vol.Schema(
|
||||
@@ -42,29 +32,14 @@ AUTH_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_get_config(hass: HomeAssistant, host: str) -> NamConfig:
|
||||
"""Get device MAC address and auth_enabled property."""
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
options = ConnectionOptions(host)
|
||||
nam = await NettigoAirMonitor.create(websession, options)
|
||||
|
||||
mac = await nam.async_get_mac_address()
|
||||
|
||||
return NamConfig(mac, nam.auth_enabled)
|
||||
|
||||
|
||||
async def async_check_credentials(
|
||||
async def async_get_nam(
|
||||
hass: HomeAssistant, host: str, data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Check if credentials are valid."""
|
||||
) -> NettigoAirMonitor:
|
||||
"""Get NAM client."""
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD))
|
||||
|
||||
nam = await NettigoAirMonitor.create(websession, options)
|
||||
|
||||
await nam.async_check_credentials()
|
||||
return await NettigoAirMonitor.create(websession, options)
|
||||
|
||||
|
||||
class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -72,8 +47,8 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_config: NamConfig
|
||||
host: str
|
||||
auth_enabled: bool = False
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -85,21 +60,20 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.host = user_input[CONF_HOST]
|
||||
|
||||
try:
|
||||
config = await async_get_config(self.hass, self.host)
|
||||
nam = await async_get_nam(self.hass, self.host, {})
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotGetMacError:
|
||||
return self.async_abort(reason="device_unsupported")
|
||||
except AuthFailedError:
|
||||
return await self.async_step_credentials()
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(format_mac(config.mac_address))
|
||||
await self.async_set_unique_id(format_mac(nam.mac))
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
|
||||
if config.auth_enabled is True:
|
||||
return await self.async_step_credentials()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.host,
|
||||
data=user_input,
|
||||
@@ -119,7 +93,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await async_check_credentials(self.hass, self.host, user_input)
|
||||
nam = await async_get_nam(self.hass, self.host, user_input)
|
||||
except AuthFailedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
@@ -128,6 +102,9 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(format_mac(nam.mac))
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.host,
|
||||
data={**user_input, CONF_HOST: self.host},
|
||||
@@ -148,14 +125,16 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._async_abort_entries_match({CONF_HOST: self.host})
|
||||
|
||||
try:
|
||||
self._config = await async_get_config(self.hass, self.host)
|
||||
nam = await async_get_nam(self.hass, self.host, {})
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except CannotGetMacError:
|
||||
return self.async_abort(reason="device_unsupported")
|
||||
except AuthFailedError:
|
||||
self.auth_enabled = True
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
await self.async_set_unique_id(format_mac(self._config.mac_address))
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
await self.async_set_unique_id(format_mac(nam.mac))
|
||||
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
@@ -171,7 +150,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data={CONF_HOST: self.host},
|
||||
)
|
||||
|
||||
if self._config.auth_enabled is True:
|
||||
if self.auth_enabled is True:
|
||||
return await self.async_step_credentials()
|
||||
|
||||
self._set_confirm_only()
|
||||
@@ -198,7 +177,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await async_check_credentials(self.hass, self.host, user_input)
|
||||
await async_get_nam(self.hass, self.host, user_input)
|
||||
except (
|
||||
ApiError,
|
||||
AuthFailedError,
|
||||
@@ -228,11 +207,11 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
config = await async_get_config(self.hass, user_input[CONF_HOST])
|
||||
nam = await async_get_nam(self.hass, user_input[CONF_HOST], {})
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(format_mac(config.mac_address))
|
||||
await self.async_set_unique_id(format_mac(nam.mac))
|
||||
self._abort_if_unique_id_mismatch(reason="another_device")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["nettigo_air_monitor"],
|
||||
"requirements": ["nettigo-air-monitor==4.1.0"],
|
||||
"requirements": ["nettigo-air-monitor==5.0.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import aiohttp
|
||||
from pynordpool import (
|
||||
Currency,
|
||||
DeliveryPeriodData,
|
||||
@@ -91,6 +92,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
|
||||
except (
|
||||
NordPoolResponseError,
|
||||
NordPoolError,
|
||||
TimeoutError,
|
||||
aiohttp.ClientError,
|
||||
) as error:
|
||||
LOGGER.debug("Connection error: %s", error)
|
||||
self.async_set_update_error(error)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
|
||||
import httpx
|
||||
import ollama
|
||||
@@ -100,8 +101,12 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
|
||||
for entry in entries:
|
||||
use_existing = False
|
||||
# Create subentry with model from entry.data and options from entry.options
|
||||
subentry_data = entry.options.copy()
|
||||
subentry_data[CONF_MODEL] = entry.data[CONF_MODEL]
|
||||
|
||||
subentry = ConfigSubentry(
|
||||
data=entry.options,
|
||||
data=MappingProxyType(subentry_data),
|
||||
subentry_type="conversation",
|
||||
title=entry.title,
|
||||
unique_id=None,
|
||||
@@ -154,9 +159,11 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
title=DEFAULT_NAME,
|
||||
# Update parent entry to only keep URL, remove model
|
||||
data={CONF_URL: entry.data[CONF_URL]},
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
version=3,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -164,7 +171,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 2:
|
||||
if entry.version > 3:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
@@ -182,6 +189,25 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
# Update subentries to include the model
|
||||
for subentry in entry.subentries.values():
|
||||
if subentry.subentry_type == "conversation":
|
||||
updated_data = dict(subentry.data)
|
||||
updated_data[CONF_MODEL] = entry.data[CONF_MODEL]
|
||||
|
||||
hass.config_entries.async_update_subentry(
|
||||
entry, subentry, data=MappingProxyType(updated_data)
|
||||
)
|
||||
|
||||
# Update main entry to remove model and bump version
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={CONF_URL: entry.data[CONF_URL]},
|
||||
version=3,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_LLM_HASS_API, CONF_NAME, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers import config_validation as cv, llm
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
NumberSelector,
|
||||
@@ -38,6 +38,7 @@ from homeassistant.helpers.selector import (
|
||||
)
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from . import OllamaConfigEntry
|
||||
from .const import (
|
||||
CONF_KEEP_ALIVE,
|
||||
CONF_MAX_HISTORY,
|
||||
@@ -72,43 +73,43 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Ollama."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
VERSION = 3
|
||||
MINOR_VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize config flow."""
|
||||
self.url: str | None = None
|
||||
self.model: str | None = None
|
||||
self.client: ollama.AsyncClient | None = None
|
||||
self.download_task: asyncio.Task | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
user_input = user_input or {}
|
||||
self.url = user_input.get(CONF_URL, self.url)
|
||||
self.model = user_input.get(CONF_MODEL, self.model)
|
||||
|
||||
if self.url is None:
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, last_step=False
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
url = user_input[CONF_URL]
|
||||
|
||||
self._async_abort_entries_match({CONF_URL: self.url})
|
||||
self._async_abort_entries_match({CONF_URL: url})
|
||||
|
||||
try:
|
||||
self.client = ollama.AsyncClient(
|
||||
host=self.url, verify=get_default_context()
|
||||
url = cv.url(url)
|
||||
except vol.Invalid:
|
||||
errors["base"] = "invalid_url"
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
response = await self.client.list()
|
||||
|
||||
downloaded_models: set[str] = {
|
||||
model_info["model"] for model_info in response.get("models", [])
|
||||
}
|
||||
try:
|
||||
client = ollama.AsyncClient(host=url, verify=get_default_context())
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
await client.list()
|
||||
except (TimeoutError, httpx.ConnectError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
@@ -117,10 +118,69 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
if self.model is None:
|
||||
return self.async_create_entry(
|
||||
title=url,
|
||||
data={CONF_URL: url},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {"conversation": ConversationSubentryFlowHandler}
|
||||
|
||||
|
||||
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Flow for managing conversation subentries."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the subentry flow."""
|
||||
super().__init__()
|
||||
self._name: str | None = None
|
||||
self._model: str | None = None
|
||||
self.download_task: asyncio.Task | None = None
|
||||
self._config_data: dict[str, Any] | None = None
|
||||
|
||||
@property
|
||||
def _is_new(self) -> bool:
|
||||
"""Return if this is a new subentry."""
|
||||
return self.source == "user"
|
||||
|
||||
@property
|
||||
def _client(self) -> ollama.AsyncClient:
|
||||
"""Return the Ollama client."""
|
||||
entry: OllamaConfigEntry = self._get_entry()
|
||||
return entry.runtime_data
|
||||
|
||||
async def async_step_set_options(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle model selection and configuration step."""
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
if user_input is None:
|
||||
# Get available models from Ollama server
|
||||
try:
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
response = await self._client.list()
|
||||
|
||||
downloaded_models: set[str] = {
|
||||
model_info["model"] for model_info in response.get("models", [])
|
||||
}
|
||||
except (TimeoutError, httpx.ConnectError, httpx.HTTPError):
|
||||
_LOGGER.exception("Failed to get models from Ollama server")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
# Show models that have been downloaded first, followed by all known
|
||||
# models (only latest tags).
|
||||
models_to_list = [
|
||||
@@ -131,52 +191,69 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
for m in sorted(MODEL_NAMES)
|
||||
if m not in downloaded_models
|
||||
]
|
||||
model_step_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_MODEL, description={"suggested_value": DEFAULT_MODEL}
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(options=models_to_list, custom_value=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if self._is_new:
|
||||
options = {}
|
||||
else:
|
||||
options = self._get_reconfigure_subentry().data.copy()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=model_step_schema,
|
||||
step_id="set_options",
|
||||
data_schema=vol.Schema(
|
||||
ollama_config_option_schema(
|
||||
self.hass, self._is_new, options, models_to_list
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
if self.model not in downloaded_models:
|
||||
# Ollama server needs to download model first
|
||||
return await self.async_step_download()
|
||||
self._model = user_input[CONF_MODEL]
|
||||
if self._is_new:
|
||||
self._name = user_input.pop(CONF_NAME)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.url,
|
||||
data={CONF_URL: self.url, CONF_MODEL: self.model},
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": {},
|
||||
"title": _get_title(self.model),
|
||||
"unique_id": None,
|
||||
}
|
||||
],
|
||||
# Check if model needs to be downloaded
|
||||
try:
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
response = await self._client.list()
|
||||
|
||||
currently_downloaded_models: set[str] = {
|
||||
model_info["model"] for model_info in response.get("models", [])
|
||||
}
|
||||
|
||||
if self._model not in currently_downloaded_models:
|
||||
# Store the user input to use after download
|
||||
self._config_data = user_input
|
||||
# Ollama server needs to download model first
|
||||
return await self.async_step_download()
|
||||
except Exception:
|
||||
_LOGGER.exception("Failed to check model availability")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
# Model is already downloaded, create/update the entry
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
async def async_step_download(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
) -> SubentryFlowResult:
|
||||
"""Step to wait for Ollama server to download a model."""
|
||||
assert self.model is not None
|
||||
assert self.client is not None
|
||||
assert self._model is not None
|
||||
|
||||
if self.download_task is None:
|
||||
# Tell Ollama server to pull the model.
|
||||
# The task will block until the model and metadata are fully
|
||||
# downloaded.
|
||||
self.download_task = self.hass.async_create_background_task(
|
||||
self.client.pull(self.model),
|
||||
f"Downloading {self.model}",
|
||||
self._client.pull(self._model),
|
||||
f"Downloading {self._model}",
|
||||
)
|
||||
|
||||
if self.download_task.done():
|
||||
@@ -192,80 +269,28 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
progress_task=self.download_task,
|
||||
)
|
||||
|
||||
async def async_step_finish(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Step after model downloading has succeeded."""
|
||||
assert self.url is not None
|
||||
assert self.model is not None
|
||||
|
||||
return self.async_create_entry(
|
||||
title=_get_title(self.model),
|
||||
data={CONF_URL: self.url, CONF_MODEL: self.model},
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": {},
|
||||
"title": _get_title(self.model),
|
||||
"unique_id": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
async def async_step_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
) -> SubentryFlowResult:
|
||||
"""Step after model downloading has failed."""
|
||||
return self.async_abort(reason="download_failed")
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {"conversation": ConversationSubentryFlowHandler}
|
||||
|
||||
|
||||
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Flow for managing conversation subentries."""
|
||||
|
||||
@property
|
||||
def _is_new(self) -> bool:
|
||||
"""Return if this is a new subentry."""
|
||||
return self.source == "user"
|
||||
|
||||
async def async_step_set_options(
|
||||
async def async_step_finish(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Set conversation options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
"""Step after model downloading has succeeded."""
|
||||
assert self._config_data is not None
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is None:
|
||||
if self._is_new:
|
||||
options = {}
|
||||
else:
|
||||
options = self._get_reconfigure_subentry().data.copy()
|
||||
|
||||
elif self._is_new:
|
||||
# Model download completed, create/update the entry with stored config
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=user_input.pop(CONF_NAME),
|
||||
data=user_input,
|
||||
title=self._name,
|
||||
data=self._config_data,
|
||||
)
|
||||
else:
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
schema = ollama_config_option_schema(self.hass, self._is_new, options)
|
||||
return self.async_show_form(
|
||||
step_id="set_options", data_schema=vol.Schema(schema), errors=errors
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=self._config_data,
|
||||
)
|
||||
|
||||
async_step_user = async_step_set_options
|
||||
@@ -273,19 +298,14 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
|
||||
def ollama_config_option_schema(
|
||||
hass: HomeAssistant, is_new: bool, options: Mapping[str, Any]
|
||||
hass: HomeAssistant,
|
||||
is_new: bool,
|
||||
options: Mapping[str, Any],
|
||||
models_to_list: list[SelectOptionDict],
|
||||
) -> dict:
|
||||
"""Ollama options schema."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
]
|
||||
|
||||
if is_new:
|
||||
schema: dict[vol.Required | vol.Optional, Any] = {
|
||||
schema: dict = {
|
||||
vol.Required(CONF_NAME, default="Ollama Conversation"): str,
|
||||
}
|
||||
else:
|
||||
@@ -293,6 +313,12 @@ def ollama_config_option_schema(
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_MODEL,
|
||||
description={"suggested_value": options.get(CONF_MODEL, DEFAULT_MODEL)},
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(options=models_to_list, custom_value=True)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PROMPT,
|
||||
description={
|
||||
@@ -304,7 +330,18 @@ def ollama_config_option_schema(
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
description={"suggested_value": options.get(CONF_LLM_HASS_API)},
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
],
|
||||
multiple=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_NUM_CTX,
|
||||
description={
|
||||
@@ -350,11 +387,3 @@ def ollama_config_option_schema(
|
||||
)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def _get_title(model: str) -> str:
|
||||
"""Get title for config entry."""
|
||||
if model.endswith(":latest"):
|
||||
model = model.split(":", maxsplit=1)[0]
|
||||
|
||||
return model
|
||||
|
||||
@@ -166,11 +166,14 @@ class OllamaBaseLLMEntity(Entity):
|
||||
self.subentry = subentry
|
||||
self._attr_name = subentry.title
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
|
||||
model, _, version = subentry.data[CONF_MODEL].partition(":")
|
||||
self._attr_device_info = dr.DeviceInfo(
|
||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
name=subentry.title,
|
||||
manufacturer="Ollama",
|
||||
model=entry.data[CONF_MODEL],
|
||||
model=model,
|
||||
sw_version=version or "latest",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,24 +3,17 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"model": "Model"
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
}
|
||||
},
|
||||
"download": {
|
||||
"title": "Downloading model"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"download_failed": "Model downloading failed",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_url": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"progress": {
|
||||
"download": "Please wait while the model is downloaded, which may take a very long time. Check your Ollama server logs for more details."
|
||||
}
|
||||
},
|
||||
"config_subentries": {
|
||||
@@ -33,6 +26,7 @@
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
"model": "Model",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"prompt": "Instructions",
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
|
||||
@@ -47,11 +41,19 @@
|
||||
"num_ctx": "Maximum number of text tokens the model can process. Lower to reduce Ollama RAM, or increase for a large number of exposed entities.",
|
||||
"think": "If enabled, the LLM will think before responding. This can improve response quality but may increase latency."
|
||||
}
|
||||
},
|
||||
"download": {
|
||||
"title": "Downloading model"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"entry_not_loaded": "Cannot add things while the configuration is disabled."
|
||||
"entry_not_loaded": "Failed to add agent. The configuration is disabled.",
|
||||
"download_failed": "Model downloading failed",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"progress": {
|
||||
"download": "Please wait while the model is downloaded, which may take a very long time. Check your Ollama server logs for more details."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class PlaystationNetworkData:
|
||||
presence: dict[str, Any] = field(default_factory=dict)
|
||||
username: str = ""
|
||||
account_id: str = ""
|
||||
available: bool = False
|
||||
availability: str = "unavailable"
|
||||
active_sessions: dict[PlatformType, SessionData] = field(default_factory=dict)
|
||||
registered_platforms: set[PlatformType] = field(default_factory=set)
|
||||
trophy_summary: TrophySummary | None = None
|
||||
@@ -92,10 +92,7 @@ class PlaystationNetwork:
|
||||
data.username = self.user.online_id
|
||||
data.account_id = self.user.account_id
|
||||
|
||||
data.available = (
|
||||
data.presence.get("basicPresence", {}).get("availability")
|
||||
== "availableToPlay"
|
||||
)
|
||||
data.availability = data.presence["basicPresence"]["availability"]
|
||||
|
||||
session = SessionData()
|
||||
session.platform = PlatformType(
|
||||
@@ -127,8 +124,6 @@ class PlaystationNetwork:
|
||||
if (game_title_info := presence[0] if presence else {}) and game_title_info[
|
||||
"onlineStatus"
|
||||
] == "online":
|
||||
data.available = True
|
||||
|
||||
platform = PlatformType(game_title_info["platform"])
|
||||
|
||||
if platform is PlatformType.PS4:
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
},
|
||||
"last_online": {
|
||||
"default": "mdi:account-clock"
|
||||
},
|
||||
"online_status": {
|
||||
"default": "mdi:account-badge",
|
||||
"state": {
|
||||
"busy": "mdi:account-cancel",
|
||||
"availabletocommunicate": "mdi:cellphone",
|
||||
"offline": "mdi:account-off-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class PsnMediaPlayerEntity(
|
||||
"""Media Player state getter."""
|
||||
session = self.coordinator.data.active_sessions.get(self.key)
|
||||
if session and session.status == "online":
|
||||
if self.coordinator.data.available and session.title_id is not None:
|
||||
if session.title_id is not None:
|
||||
return MediaPlayerState.PLAYING
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@@ -37,6 +37,7 @@ class PlaystationNetworkSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
value_fn: Callable[[PlaystationNetworkData], StateType | datetime]
|
||||
entity_picture: str | None = None
|
||||
available_fn: Callable[[PlaystationNetworkData], bool] = lambda _: True
|
||||
|
||||
|
||||
class PlaystationNetworkSensor(StrEnum):
|
||||
@@ -50,6 +51,7 @@ class PlaystationNetworkSensor(StrEnum):
|
||||
EARNED_TROPHIES_BRONZE = "earned_trophies_bronze"
|
||||
ONLINE_ID = "online_id"
|
||||
LAST_ONLINE = "last_online"
|
||||
ONLINE_STATUS = "online_status"
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
@@ -117,8 +119,16 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
psn.presence["basicPresence"]["lastAvailableDate"]
|
||||
)
|
||||
),
|
||||
available_fn=lambda psn: "lastAvailableDate" in psn.presence["basicPresence"],
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.ONLINE_STATUS,
|
||||
translation_key=PlaystationNetworkSensor.ONLINE_STATUS,
|
||||
value_fn=lambda psn: psn.availability.lower().replace("unavailable", "offline"),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["offline", "availabletoplay", "availabletocommunicate", "busy"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -183,3 +193,12 @@ class PlaystationNetworkSensorEntity(
|
||||
)
|
||||
|
||||
return super().entity_picture
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
|
||||
return (
|
||||
self.entity_description.available_fn(self.coordinator.data)
|
||||
and super().available
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user