Compare commits

..

46 Commits

Author SHA1 Message Date
epenet
7b2e4dbe16 Ruff 2025-10-17 13:46:52 +00:00
epenet
1f812e9f89 Fix motor reverse mode logic 2025-10-17 14:54:20 +02:00
johanzander
b182d5ce87 Add additional unit tests for Growatt Server integration (#154644) 2025-10-17 14:22:16 +02:00
Thomas55555
175365bdea Add integration_type to Husqvarna Automower (#154642) 2025-10-17 14:18:32 +02:00
Bouwe Westerdijk
cbe52cbfca Bump plugwise to v1.8.1 (#154679) 2025-10-17 15:13:35 +03:00
Felipe Santos
9251dde2c6 Add OpenRGB reconfiguration flow (#154478) 2025-10-17 12:27:11 +02:00
Andrew Jackson
24d77cc453 Bump aiomealie to 1.0.1 (#154672) 2025-10-17 12:23:55 +03:00
johanzander
a1f98abe49 Add CODEOWNERS entry for Growatt Server integration (#154647) 2025-10-17 11:20:11 +03:00
cdnninja
d25dde1d11 Bump pyvesync version to 3.1.2 (#154650)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-17 10:19:48 +02:00
hanwg
8ec483b38b Fix Telegram bot bug where message is sent to wrong recipient (#154658) 2025-10-17 11:15:41 +03:00
epenet
bf14caca69 Fix behavior spelling for public facing strings (#154665) 2025-10-17 11:07:05 +03:00
Ludovic BOUÉ
e5fb6b2fb2 Remove duplicated Matter powersource cluster from Mock device fixture files (#154668) 2025-10-17 11:06:01 +03:00
epenet
7dfeb3a3f6 Improve metoffice typing (#154670) 2025-10-17 10:05:27 +02:00
epenet
9d3b1562c4 Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154667) 2025-10-17 09:46:53 +02:00
epenet
e14407f066 Remove HomeAssistantRemoteScanner from __all__ in bluetooth (#154669) 2025-10-17 09:31:30 +02:00
epenet
67872e3746 Adjust onewire strings (#154664) 2025-10-17 09:28:37 +02:00
Manu
06bd1a2003 Migrate Xbox to runtime_data (#154652) 2025-10-17 09:25:49 +02:00
dependabot[bot]
37ea360304 Bump sigstore/cosign-installer from 3.10.0 to 4.0.0 (#154661)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 09:15:41 +02:00
epenet
25ce57424c Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154660) 2025-10-17 08:35:18 +02:00
Thomas55555
3d46ab549d Add serial number to IPP (#154648) 2025-10-16 23:58:57 +01:00
Thomas55555
567cc9f842 Bump colorlog to 6.10.1 (#154643) 2025-10-16 23:57:24 +01:00
Shay Levy
b5457a5abd Fix demo cover set position action (#154641) 2025-10-16 21:21:32 +03:00
Marc Mueller
e4b5e35d1d Update Pillow to 12.0.0 (#154637) 2025-10-16 18:25:36 +01:00
Ludovic BOUÉ
12023c33b5 Rename Mock Door Lock with unbolt fixture (#154627) 2025-10-16 13:01:46 -04:00
Jan Čermák
a28749937c Allow ignored rapt_ble devices to be set up from the user flow (#154606) 2025-10-16 12:54:24 -04:00
Jan Čermák
3fe37d651f Update Home Assistant base image to 2025.10.1 (#154609)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 12:53:25 -04:00
epenet
cb3424cdf0 Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154622) 2025-10-16 12:52:51 -04:00
Thomas D
a799f7ff91 Add service warning sensor to Volvo integration (#154613) 2025-10-16 18:52:12 +02:00
Louis Pré
34ab725b75 LLM prefix caching optimization using new GetDateTime tool (#152408)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com>
2025-10-16 12:47:12 -04:00
Manu
2dfc7f02ba Bump habiticalib to v0.4.6 (#154566) 2025-10-16 17:15:13 +01:00
Jan Čermák
c8919222bd Mock network calls in comfoconnect tests to fix timeouts (#154620) 2025-10-16 11:42:04 -04:00
Ludovic BOUÉ
a888264d2f Add Matter fixture for Aqara Smart Lock U200 (#154623) 2025-10-16 16:25:16 +02:00
Joost Lekkerkerker
ae84c7e15d Add subentries to WAQI (#148966)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 14:11:52 +01:00
epenet
415c8b490b Add device diagnostics to onewire (#154617)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 14:56:19 +02:00
Aviad Levy
6038f15406 Add support for Telegram message attachments (#153216) 2025-10-16 14:54:50 +02:00
Justus
a8758253c4 Add config flow exceptions to IOMeter (#154604)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 14:52:51 +02:00
epenet
fa4eb2e820 The 1-wire integration has now reached silver on the quality scale (#154614) 2025-10-16 14:52:11 +02:00
Ludovic BOUÉ
58f35d0614 Add Matter Eve Energy 20ECN4101 fixture (#154608) 2025-10-16 14:07:29 +02:00
epenet
f72a91ca29 Remove assist_pipeline from _IGNORE_ROOT_IMPORT in pylint plugin (#154600) 2025-10-16 13:33:19 +02:00
Thomas D
5d99da6e1f The Volvo integration has now reached platinum on the quality scale (#154015)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-16 13:07:54 +02:00
Joost Lekkerkerker
64746eb99c Add new Dryer fixture to SmartThings (#154607) 2025-10-16 12:55:30 +02:00
Maciej Bieniek
70fc6df599 Make Shelly deprecated firmware issue more general (#154539) 2025-10-16 13:50:43 +03:00
epenet
8dc33ece7b Remove sensor from _IGNORE_ROOT_IMPORT in pylint plugin (#154602) 2025-10-16 11:28:29 +01:00
Carlos Gustavo Sarmiento
3d4d8e7f20 Make Speed optional for GoToPreset ONVIF command (#149636)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 11:21:02 +01:00
Joakim Sørensen
c92d319e12 Bump hass-nabucasa from 1.3.0 to 1.4.0 (#154599) 2025-10-16 11:18:55 +01:00
Christopher Fenner
1bdba7906a Add new sensors for Zigbee based devices in ViCare (#154271) 2025-10-16 11:11:08 +01:00
185 changed files with 22185 additions and 771 deletions

View File

@@ -326,7 +326,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.2.3"

View File

@@ -689,14 +689,14 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint homeassistant
pylint --ignore-missing-annotations=y homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint-tests:
name: Check pylint on tests

2
CODEOWNERS generated
View File

@@ -619,6 +619,8 @@ build.json @home-assistant/supervisor
/tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core
/tests/components/group/ @home-assistant/core
/homeassistant/components/growatt_server/ @johanzander
/tests/components/growatt_server/ @johanzander
/homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @tr4nt0r

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -29,7 +29,7 @@
},
"data_description": {
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0"
}
}
}

View File

@@ -41,6 +41,8 @@ from .pipeline import (
async_setup_pipeline_store,
async_update_pipeline,
)
from .select import AssistPipelineSelect, VadSensitivitySelect
from .vad import VadSensitivity
from .websocket_api import async_register_websocket_api
__all__ = (
@@ -51,11 +53,14 @@ __all__ = (
"SAMPLE_CHANNELS",
"SAMPLE_RATE",
"SAMPLE_WIDTH",
"AssistPipelineSelect",
"AudioSettings",
"Pipeline",
"PipelineEvent",
"PipelineEventType",
"PipelineNotFound",
"VadSensitivity",
"VadSensitivitySelect",
"WakeWordSettings",
"async_create_default_pipeline",
"async_get_pipelines",

View File

@@ -146,7 +146,7 @@
},
"state": {
"title": "Add a Bayesian sensor",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behavior can be overridden by adding observations for the same entity's other states.",
"data": {
"name": "[%key:common::config_flow::data::name%]",

View File

@@ -113,7 +113,6 @@ __all__ = [
"BluetoothServiceInfo",
"BluetoothServiceInfoBleak",
"HaBluetoothConnector",
"HomeAssistantRemoteScanner",
"async_address_present",
"async_ble_device_from_address",
"async_clear_address_from_match_history",

View File

@@ -74,7 +74,10 @@ from .const import (
StreamType,
)
from .helper import get_camera_from_entity_id
from .img_util import scale_jpeg_camera_image
from .img_util import (
TurboJPEGSingleton, # noqa: F401
scale_jpeg_camera_image,
)
from .prefs import (
CameraPreferences,
DynamicStreamSettings, # noqa: F401

View File

@@ -19,7 +19,7 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera.webrtc import async_register_ice_servers
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback

View File

@@ -12,7 +12,9 @@ from hass_nabucasa.google_report_state import ErrorResponse
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.google_assistant.helpers import ( # pylint: disable=hass-component-root-import
AbstractConfig,
)
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.3.0"],
"requirements": ["hass-nabucasa==1.4.0"],
"single_config_entry": true
}

View File

@@ -11,7 +11,7 @@ from hass_nabucasa.voice import MAP_VOICE, Gender
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.models import User
from homeassistant.components import webhook
from homeassistant.components.google_assistant.http import (
from homeassistant.components.google_assistant.http import ( # pylint: disable=hass-component-root-import
async_get_users as async_get_google_assistant_users,
)
from homeassistant.core import HomeAssistant, callback

View File

@@ -6,7 +6,9 @@ from typing import Any
import uuid
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.automation.config import async_validate_config_item
from homeassistant.components.automation.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.script.config import async_validate_config_item
from homeassistant.components.script.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.config import SCRIPT_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -569,14 +569,17 @@ class ChatLog:
if llm_api:
prompt_parts.append(llm_api.api_prompt)
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.BASE_PROMPT,
llm_context.language,
user_name,
# Append current date and time to the prompt if the corresponding tool is not provided
llm_tools: list[llm.Tool] = llm_api.tools if llm_api else []
if not any(tool.name.endswith("GetDateTime") for tool in llm_tools):
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.DATE_TIME_PROMPT,
llm_context.language,
user_name,
)
)
)
if extra_system_prompt := (
# Take new system prompt if one was given

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
import datetime
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.manual.alarm_control_panel import ManualAlarm
from homeassistant.components.manual.alarm_control_panel import ( # pylint: disable=hass-component-root-import
ManualAlarm,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME
from homeassistant.core import HomeAssistant

View File

@@ -139,6 +139,7 @@ class DemoCover(CoverEntity):
self.async_write_ha_state()
return
self._is_opening = False
self._is_closing = True
self._listen_cover()
self._requested_closing = True
@@ -162,6 +163,7 @@ class DemoCover(CoverEntity):
return
self._is_opening = True
self._is_closing = False
self._listen_cover()
self._requested_closing = False
self.async_write_ha_state()
@@ -181,10 +183,14 @@ class DemoCover(CoverEntity):
if self._position == position:
return
self._is_closing = position < (self._position or 0)
self._is_opening = not self._is_closing
self._listen_cover()
self._requested_closing = (
self._position is not None and position < self._position
)
self.async_write_ha_state()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover til to a specific position."""

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
"requirements": ["pydoods==1.0.2", "Pillow==12.0.0"]
}

View File

@@ -8,8 +8,11 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterErrorCode
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -16,7 +16,9 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.components.sensor.recorder import reset_detected
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
reset_detected,
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume
from homeassistant.core import (
HomeAssistant,

View File

@@ -10,8 +10,8 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.components.sensor.const import SensorStateClass
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -6,7 +6,7 @@ from dataclasses import replace
from aioesphomeapi import EntityInfo, SelectInfo, SelectState
from homeassistant.components.assist_pipeline.select import (
from homeassistant.components.assist_pipeline import (
AssistPipelineSelect,
VadSensitivitySelect,
)

View File

@@ -19,7 +19,9 @@ from homeassistant.components.ffmpeg import (
FFmpegManager,
get_ffmpeg_manager,
)
from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor
from homeassistant.components.ffmpeg_motion.binary_sensor import ( # pylint: disable=hass-component-root-import
FFmpegBinarySensor,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv

View File

@@ -4,8 +4,12 @@ from __future__ import annotations
from pyfirefly.models import Account, Category
from homeassistant.components.sensor import SensorEntity, SensorStateClass, StateType
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
StateType,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -6,9 +6,8 @@ import logging
from typing import Any
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.ffmpeg.camera import (
CONF_EXTRA_ARGUMENTS,
CONF_INPUT,
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT
from homeassistant.components.ffmpeg.camera import ( # pylint: disable=hass-component-root-import
DEFAULT_ARGUMENTS,
FFmpegCamera,
)

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
"requirements": ["av==13.1.0", "Pillow==12.0.0"]
}

View File

@@ -30,8 +30,8 @@ from homeassistant.components.camera import (
WebRTCMessage,
WebRTCSendMessage,
async_register_webrtc_provider,
get_dynamic_camera_stream_settings,
)
from homeassistant.components.camera.prefs import get_dynamic_camera_stream_settings
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
from homeassistant.components.stream import Orientation
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry

View File

@@ -1,7 +1,7 @@
{
"domain": "growatt_server",
"name": "Growatt",
"codeowners": [],
"codeowners": ["@johanzander"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
"iot_class": "cloud_polling",

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.5"]
"requirements": ["habiticalib==0.4.6"]
}

View File

@@ -27,7 +27,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
)
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.device_automation.trigger import (
from homeassistant.components.device_automation.trigger import ( # pylint: disable=hass-component-root-import
async_validate_trigger_config,
)
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass

View File

@@ -5,6 +5,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==11.3.0"]
"requirements": ["Pillow==12.0.0"]
}

View File

@@ -2,7 +2,12 @@
from typing import Any, Final
from iometer import IOmeterClient, IOmeterConnectionError
from iometer import (
IOmeterClient,
IOmeterConnectionError,
IOmeterNoReadingsError,
IOmeterNoStatusError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -34,6 +39,11 @@ class IOMeterConfigFlow(ConfigFlow, domain=DOMAIN):
client = IOmeterClient(host=host, session=session)
try:
status = await client.get_current_status()
_ = await client.get_current_reading()
except IOmeterNoStatusError:
return self.async_abort(reason="no_status")
except IOmeterNoReadingsError:
return self.async_abort(reason="no_readings")
except IOmeterConnectionError:
return self.async_abort(reason="cannot_connect")
@@ -70,6 +80,11 @@ class IOMeterConfigFlow(ConfigFlow, domain=DOMAIN):
client = IOmeterClient(host=self._host, session=session)
try:
status = await client.get_current_status()
_ = await client.get_current_reading()
except IOmeterNoStatusError:
errors["base"] = "no_status"
except IOmeterNoReadingsError:
errors["base"] = "no_readings"
except IOmeterConnectionError:
errors["base"] = "cannot_connect"
else:

View File

@@ -20,6 +20,8 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
},
"error": {
"no_status": "No status received from the IOmeter. Check your device status in the IOmeter app",
"no_readings": "No readings received from the IOmeter. Please attach the IOmeter Core to the electricity meter and wait for the first reading.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}

View File

@@ -31,6 +31,7 @@ class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]):
manufacturer=self.coordinator.data.info.manufacturer,
model=self.coordinator.data.info.model,
name=self.coordinator.data.info.name,
serial_number=self.coordinator.data.info.serial,
sw_version=self.coordinator.data.info.version,
configuration_url=self.coordinator.data.info.more_info,
)

View File

@@ -358,7 +358,7 @@
"entity_label": "Entity name",
"entity_description": "Optional if a device is selected, otherwise required. If the entity is assigned to a device, the device name is used as prefix.",
"entity_category_title": "Entity category",
"entity_category_description": "Classification of a non-primary entity. Leave empty for standard behaviour."
"entity_category_description": "Classification of a non-primary entity. Leave empty for standard behavior."
},
"knx": {
"title": "KNX configuration",

View File

@@ -8,7 +8,7 @@ from pychromecast import Chromecast
from pychromecast.const import CAST_TYPE_CHROMECAST
from homeassistant.components.cast import DOMAIN as CAST_DOMAIN
from homeassistant.components.cast.home_assistant_cast import (
from homeassistant.components.cast.home_assistant_cast import ( # pylint: disable=hass-component-root-import
ATTR_URL_PATH,
ATTR_VIEW_PATH,
NO_URL_AVAILABLE_ERROR,

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
"quality_scale": "legacy",
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
"requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["aiomealie==1.0.0"]
"requirements": ["aiomealie==1.0.1"]
}

View File

@@ -5,9 +5,8 @@ from __future__ import annotations
import asyncio
import logging
import datapoint
import datapoint.Forecast
import datapoint.Manager
from datapoint.Forecast import Forecast
from datapoint.Manager import Manager
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -48,19 +47,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinates = f"{latitude}_{longitude}"
connection = datapoint.Manager.Manager(api_key=api_key)
connection = Manager(api_key=api_key)
async def async_update_hourly() -> datapoint.Forecast:
async def async_update_hourly() -> Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "hourly"
)
async def async_update_daily() -> datapoint.Forecast:
async def async_update_daily() -> Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "daily"
)
async def async_update_twice_daily() -> datapoint.Forecast:
async def async_update_twice_daily() -> Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "twice-daily"
)

View File

@@ -6,9 +6,8 @@ from collections.abc import Mapping
import logging
from typing import Any
import datapoint
from datapoint.exceptions import APIException
import datapoint.Manager
from datapoint.Manager import Manager
from requests import HTTPError
import voluptuous as vol
@@ -31,7 +30,7 @@ async def validate_input(
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
errors = {}
connection = datapoint.Manager.Manager(api_key=api_key)
connection = Manager(api_key=api_key)
try:
forecast = await hass.async_add_executor_job(

View File

@@ -5,8 +5,9 @@ from __future__ import annotations
import logging
from typing import Any, Literal
import datapoint
from datapoint.exceptions import APIException
from datapoint.Forecast import Forecast
from datapoint.Manager import Manager
from requests import HTTPError
from homeassistant.exceptions import ConfigEntryAuthFailed
@@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
def fetch_data(
connection: datapoint.Manager,
connection: Manager,
latitude: float,
longitude: float,
frequency: Literal["daily", "twice-daily", "hourly"],
@@ -26,7 +27,7 @@ def fetch_data(
return connection.get_forecast(
latitude, longitude, frequency, convert_weather_code=False
)
except (ValueError, datapoint.exceptions.APIException) as err:
except (ValueError, APIException) as err:
_LOGGER.error("Check Met Office connection: %s", err.args)
raise UpdateFailed from err
except HTTPError as err:

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, cast
from datapoint.Forecast import Forecast as ForecastData
from datapoint.Forecast import Forecast
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
@@ -22,7 +22,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_WIND_BEARING,
DOMAIN as WEATHER_DOMAIN,
CoordinatorWeatherEntity,
Forecast,
Forecast as WeatherForecast,
WeatherEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
@@ -85,20 +85,20 @@ async def async_setup_entry(
)
def _build_hourly_forecast_data(timestep: dict[str, Any]) -> Forecast:
data = Forecast(datetime=timestep["time"].isoformat())
def _build_hourly_forecast_data(timestep: dict[str, Any]) -> WeatherForecast:
data = WeatherForecast(datetime=timestep["time"].isoformat())
_populate_forecast_data(data, timestep, HOURLY_FORECAST_ATTRIBUTE_MAP)
return data
def _build_daily_forecast_data(timestep: dict[str, Any]) -> Forecast:
data = Forecast(datetime=timestep["time"].isoformat())
def _build_daily_forecast_data(timestep: dict[str, Any]) -> WeatherForecast:
data = WeatherForecast(datetime=timestep["time"].isoformat())
_populate_forecast_data(data, timestep, DAILY_FORECAST_ATTRIBUTE_MAP)
return data
def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> Forecast:
data = Forecast(datetime=timestep["time"].isoformat())
def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> WeatherForecast:
data = WeatherForecast(datetime=timestep["time"].isoformat())
# day and night forecasts have slightly different format
if "daySignificantWeatherCode" in timestep:
@@ -111,7 +111,7 @@ def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> Forecast:
def _populate_forecast_data(
forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str]
forecast: WeatherForecast, timestep: dict[str, Any], mapping: dict[str, str]
) -> None:
def get_mapped_attribute(attr: str) -> Any:
if attr not in mapping:
@@ -153,9 +153,9 @@ def _populate_forecast_data(
class MetOfficeWeather(
CoordinatorWeatherEntity[
TimestampDataUpdateCoordinator[ForecastData],
TimestampDataUpdateCoordinator[ForecastData],
TimestampDataUpdateCoordinator[ForecastData],
TimestampDataUpdateCoordinator[Forecast],
TimestampDataUpdateCoordinator[Forecast],
TimestampDataUpdateCoordinator[Forecast],
]
):
"""Implementation of a Met Office weather condition."""
@@ -177,9 +177,9 @@ class MetOfficeWeather(
def __init__(
self,
coordinator_daily: TimestampDataUpdateCoordinator[ForecastData],
coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData],
coordinator_twice_daily: TimestampDataUpdateCoordinator[ForecastData],
coordinator_daily: TimestampDataUpdateCoordinator[Forecast],
coordinator_hourly: TimestampDataUpdateCoordinator[Forecast],
coordinator_twice_daily: TimestampDataUpdateCoordinator[Forecast],
hass_data: dict[str, Any],
) -> None:
"""Initialise the platform with a data instance."""
@@ -263,10 +263,10 @@ class MetOfficeWeather(
return float(value) if value is not None else None
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
def _async_forecast_daily(self) -> list[WeatherForecast] | None:
"""Return the daily forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[ForecastData],
TimestampDataUpdateCoordinator[Forecast],
self.forecast_coordinators["daily"],
)
timesteps = coordinator.data.timesteps
@@ -277,10 +277,10 @@ class MetOfficeWeather(
]
@callback
def _async_forecast_hourly(self) -> list[Forecast] | None:
def _async_forecast_hourly(self) -> list[WeatherForecast] | None:
"""Return the hourly forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[ForecastData],
TimestampDataUpdateCoordinator[Forecast],
self.forecast_coordinators["hourly"],
)
@@ -292,10 +292,10 @@ class MetOfficeWeather(
]
@callback
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
def _async_forecast_twice_daily(self) -> list[WeatherForecast] | None:
"""Return the twice daily forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[ForecastData],
TimestampDataUpdateCoordinator[Forecast],
self.forecast_coordinators["twice_daily"],
)
timesteps = coordinator.data.timesteps

View File

@@ -8,6 +8,7 @@ from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .onewirehub import OneWireConfigEntry
@@ -26,7 +27,28 @@ async def async_get_config_entry_diagnostics(
"data": async_redact_data(entry.data, TO_REDACT),
"options": {**entry.options},
},
"devices": [asdict(device_details) for device_details in onewire_hub.devices]
if onewire_hub.devices
else [],
"devices": [asdict(device_details) for device_details in onewire_hub.devices],
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: OneWireConfigEntry, device_entry: dr.DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
onewire_hub = entry.runtime_data
return {
"entry": {
"title": entry.title,
"data": async_redact_data(entry.data, TO_REDACT),
"options": {**entry.options},
},
"device": asdict(
next(
device_details
for device_details in onewire_hub.devices
if device_details.id[3:] == device_entry.serial_number
)
),
}

View File

@@ -7,6 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aio_ownet"],
"quality_scale": "silver",
"requirements": ["aio-ownet==0.0.4"],
"zeroconf": ["_owserver._tcp.local."]
}

View File

@@ -1,8 +1,6 @@
rules:
## Bronze
config-flow:
status: todo
comment: missing data_description on options flow
config-flow: done
test-before-configure: done
unique-config-entry:
status: done
@@ -16,27 +14,19 @@ rules:
entity-event-setup:
status: exempt
comment: entities do not subscribe to events
dependency-transparency:
status: todo
comment: The package is not built and published inside a CI pipeline
dependency-transparency: done
action-setup:
status: exempt
comment: No service actions currently available
common-modules:
status: done
comment: base entity available, but no coordinator
docs-high-level-description:
status: todo
comment: Under review
docs-installation-instructions:
status: todo
comment: Under review
docs-removal-instructions:
status: todo
comment: Under review
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-actions:
status: todo
comment: Under review
status: exempt
comment: No service actions currently available
brands: done
## Silver
@@ -52,12 +42,8 @@ rules:
parallel-updates: done
test-coverage: done
integration-owner: done
docs-installation-parameters:
status: todo
comment: Under review
docs-configuration-parameters:
status: todo
comment: Under review
docs-installation-parameters: done
docs-configuration-parameters: done
## Gold
entity-translations: done
@@ -73,9 +59,7 @@ rules:
comment: >
Manual removal, as it is not possible to distinguish
between a flaky device and a device that has been removed
diagnostics:
status: todo
comment: config-entry diagnostics level available, might be nice to have device-level diagnostics
diagnostics: done
exception-translations:
status: todo
comment: Under review

View File

@@ -139,8 +139,12 @@
"step": {
"device_selection": {
"data": {
"clear_device_options": "Clear all device configurations",
"device_selection": "[%key:component::onewire::options::error::device_not_selected%]"
"clear_device_options": "Reset all device customizations",
"device_selection": "Customize specific devices"
},
"data_description": {
"clear_device_options": "Use this to reset all device specific options to default values.",
"device_selection": "Customize behavior of individual devices."
},
"description": "Select what configuration steps to process",
"title": "1-Wire device options"
@@ -149,6 +153,9 @@
"data": {
"precision": "Sensor precision"
},
"data_description": {
"precision": "The lower the precision, the faster the sensor will respond, but with less accuracy."
},
"description": "Select sensor precision for {sensor_id}",
"title": "1-Wire sensor precision"
}

View File

@@ -70,7 +70,7 @@ async def async_setup_entry(
vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]),
vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]),
vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float,
vol.Optional(ATTR_SPEED, default=0.5): cv.small_float,
vol.Optional(ATTR_SPEED): cv.small_float,
vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In(
[
CONTINUOUS_MOVE,
@@ -210,10 +210,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
async def async_perform_ptz(
self,
distance,
speed,
move_mode,
continuous_duration,
preset,
speed=None,
pan=None,
tilt=None,
zoom=None,

View File

@@ -602,10 +602,11 @@ class ONVIFDevice:
return
req.PresetToken = preset_val
req.Speed = {
"PanTilt": {"x": speed_val, "y": speed_val},
"Zoom": {"x": speed_val},
}
if speed_val is not None:
req.Speed = {
"PanTilt": {"x": speed_val, "y": speed_val},
"Zoom": {"x": speed_val},
}
await ptz_service.GotoPreset(req)
elif move_mode == STOP_MOVE:
await ptz_service.Stop(req)

View File

@@ -30,7 +30,6 @@ ptz:
max: 1
step: 0.01
speed:
default: 0.5
selector:
number:
min: 0

View File

@@ -25,6 +25,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
}
)
STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
async def validate_input(hass: HomeAssistant, host: str, port: int) -> None:
"""Validate the user input allows us to connect."""
@@ -39,6 +46,48 @@ async def validate_input(hass: HomeAssistant, host: str, port: int) -> None:
class OpenRGBConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OpenRGB."""
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the OpenRGB SDK Server."""
reconfigure_entry = self._get_reconfigure_entry()
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
# Prevent duplicate entries
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
try:
await validate_input(self.hass, host, port)
except CONNECTION_ERRORS:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception(
"Unknown error while connecting to OpenRGB SDK server at %s",
f"{host}:{port}",
)
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={CONF_HOST: host, CONF_PORT: port},
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
data_schema=STEP_RECONFIGURE_DATA_SCHEMA,
suggested_values={
CONF_HOST: reconfigure_entry.data[CONF_HOST],
CONF_PORT: reconfigure_entry.data[CONF_PORT],
},
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

View File

@@ -68,7 +68,7 @@ rules:
entity-translations: todo
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues: todo
stale-devices: todo

View File

@@ -2,6 +2,7 @@
"config": {
"step": {
"user": {
"title": "Set up OpenRGB SDK server",
"description": "Set up your OpenRGB SDK server to allow control from within Home Assistant.",
"data": {
"name": "[%key:common::config_flow::data::name%]",
@@ -13,6 +14,18 @@
"host": "The IP address or hostname of the computer running the OpenRGB SDK server.",
"port": "The port number that the OpenRGB SDK server is running on."
}
},
"reconfigure": {
"title": "Reconfigure OpenRGB SDK server",
"description": "Update the connection settings for your OpenRGB SDK server.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "[%key:component::openrgb::config::step::user::data_description::host%]",
"port": "[%key:component::openrgb::config::step::user::data_description::port%]"
}
}
},
"error": {
@@ -21,6 +34,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},

View File

@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["plugwise"],
"quality_scale": "platinum",
"requirements": ["plugwise==1.8.0"],
"requirements": ["plugwise==1.8.1"],
"zeroconf": ["_plugwise._tcp.local."]
}

View File

@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
"quality_scale": "legacy",
"requirements": ["Pillow==11.3.0"]
"requirements": ["Pillow==12.0.0"]
}

View File

@@ -6,5 +6,5 @@
"iot_class": "calculated",
"loggers": ["pyzbar"],
"quality_scale": "legacy",
"requirements": ["Pillow==11.3.0", "pyzbar==0.1.7"]
"requirements": ["Pillow==12.0.0", "pyzbar==0.1.7"]
}

View File

@@ -72,7 +72,7 @@ class RAPTPillConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
current_addresses = self._async_current_ids()
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["Pillow==11.3.0"]
"requirements": ["Pillow==12.0.0"]
}

View File

@@ -59,8 +59,8 @@ from .coordinator import (
)
from .repairs import (
async_manage_ble_scanner_firmware_unsupported_issue,
async_manage_deprecated_firmware_issue,
async_manage_outbound_websocket_incorrectly_enabled_issue,
async_manage_wall_display_firmware_unsupported_issue,
)
from .utils import (
async_create_issue_unsupported_firmware,
@@ -337,7 +337,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
await hass.config_entries.async_forward_entry_setups(
entry, runtime_data.platforms
)
async_manage_wall_display_firmware_unsupported_issue(hass, entry)
async_manage_deprecated_firmware_issue(hass, entry)
async_manage_ble_scanner_firmware_unsupported_issue(
hass,
entry,

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from enum import StrEnum
from logging import Logger, getLogger
import re
from typing import Final
from typing import Final, TypedDict
from aioshelly.const import (
MODEL_BULB,
@@ -232,7 +232,6 @@ class BLEScannerMode(StrEnum):
BLE_SCANNER_MIN_FIRMWARE = "1.5.1"
WALL_DISPLAY_MIN_FIRMWARE = "2.3.0"
MAX_PUSH_UPDATE_FAILURES = 5
PUSH_UPDATE_ISSUE_ID = "push_update_{unique}"
@@ -245,9 +244,28 @@ BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID = "ble_scanner_firmware_unsupported_{u
OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID = (
"outbound_websocket_incorrectly_enabled_{unique}"
)
WALL_DISPLAY_FIRMWARE_UNSUPPORTED_ISSUE_ID = (
"wall_display_firmware_unsupported_{unique}"
)
DEPRECATED_FIRMWARE_ISSUE_ID = "deprecated_firmware_{unique}"
class DeprecatedFirmwareInfo(TypedDict):
"""TypedDict for Deprecated Firmware Info."""
min_firmware: str
ha_version: str
# Provide firmware deprecation data:
# key: device model
# value: dict with:
# min_firmware: minimum supported firmware version
# ha_version: Home Assistant version when older firmware will be deprecated
# Example:
# DEPRECATED_FIRMWARES: dict[str, DeprecatedFirmwareInfo] = {
# MODEL_WALL_DISPLAY: DeprecatedFirmwareInfo(
# {"min_firmware": "2.3.0", "ha_version": "2025.10.0"}
# ),
# }
DEPRECATED_FIRMWARES: dict[str, DeprecatedFirmwareInfo] = {}
GAS_VALVE_OPEN_STATES = ("opening", "opened")

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from aioshelly.const import MODEL_OUT_PLUG_S_G3, MODEL_PLUG_S_G3, MODEL_WALL_DISPLAY
from aioshelly.const import MODEL_OUT_PLUG_S_G3, MODEL_PLUG_S_G3
from aioshelly.exceptions import DeviceConnectionError, RpcCallError
from aioshelly.rpc_device import RpcDevice
from awesomeversion import AwesomeVersion
@@ -19,10 +19,10 @@ from .const import (
BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID,
BLE_SCANNER_MIN_FIRMWARE,
CONF_BLE_SCANNER_MODE,
DEPRECATED_FIRMWARE_ISSUE_ID,
DEPRECATED_FIRMWARES,
DOMAIN,
OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID,
WALL_DISPLAY_FIRMWARE_UNSUPPORTED_ISSUE_ID,
WALL_DISPLAY_MIN_FIRMWARE,
BLEScannerMode,
)
from .coordinator import ShellyConfigEntry
@@ -70,21 +70,25 @@ def async_manage_ble_scanner_firmware_unsupported_issue(
@callback
def async_manage_wall_display_firmware_unsupported_issue(
def async_manage_deprecated_firmware_issue(
hass: HomeAssistant,
entry: ShellyConfigEntry,
) -> None:
"""Manage the Wall Display firmware unsupported issue."""
issue_id = WALL_DISPLAY_FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
"""Manage deprecated firmware issue."""
issue_id = DEPRECATED_FIRMWARE_ISSUE_ID.format(unique=entry.unique_id)
if TYPE_CHECKING:
assert entry.runtime_data.rpc is not None
device = entry.runtime_data.rpc.device
model = entry.data["model"]
if model in DEPRECATED_FIRMWARES:
min_firmware = DEPRECATED_FIRMWARES[model]["min_firmware"]
ha_version = DEPRECATED_FIRMWARES[model]["ha_version"]
if entry.data["model"] == MODEL_WALL_DISPLAY:
firmware = AwesomeVersion(device.shelly["ver"])
if firmware < WALL_DISPLAY_MIN_FIRMWARE:
if firmware < min_firmware:
ir.async_create_issue(
hass,
DOMAIN,
@@ -92,11 +96,12 @@ def async_manage_wall_display_firmware_unsupported_issue(
is_fixable=True,
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
translation_key="wall_display_firmware_unsupported",
translation_key="deprecated_firmware",
translation_placeholders={
"device_name": device.name,
"ip_address": device.ip_address,
"firmware": firmware,
"ha_version": ha_version,
},
data={"entry_id": entry.entry_id},
)
@@ -241,7 +246,7 @@ async def async_create_fix_flow(
if (
"ble_scanner_firmware_unsupported" in issue_id
or "wall_display_firmware_unsupported" in issue_id
or "deprecated_firmware" in issue_id
):
return FirmwareUpdateFlow(device)

View File

@@ -312,13 +312,13 @@
}
}
},
"wall_display_firmware_unsupported": {
"deprecated_firmware": {
"title": "{device_name} is running outdated firmware",
"fix_flow": {
"step": {
"confirm": {
"title": "{device_name} is running outdated firmware",
"description": "Your Shelly device {device_name} with IP address {ip_address} is running firmware {firmware}. This firmware version will not be supported by Shelly integration starting from Home Assistant 2025.11.0.\n\nSelect **Submit** button to start the OTA update to the latest stable firmware version."
"description": "Your Shelly device {device_name} with IP address {ip_address} is running firmware {firmware}. This firmware version will not be supported by Shelly integration starting from Home Assistant {ha_version}.\n\nSelect **Submit** button to start the OTA update to the latest stable firmware version."
}
},
"abort": {

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["simplehound"],
"quality_scale": "legacy",
"requirements": ["Pillow==11.3.0", "simplehound==0.3"]
"requirements": ["Pillow==12.0.0", "simplehound==0.3"]
}

View File

@@ -39,7 +39,9 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.components.plex import PLEX_URI_SCHEME
from homeassistant.components.plex.services import process_plex_payload
from homeassistant.components.plex.services import ( # pylint: disable=hass-component-root-import
process_plex_payload,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er

View File

@@ -441,9 +441,7 @@ class KeyFrameConverter:
# Keep import here so that we can import stream integration
# without installing reqs
from homeassistant.components.camera.img_util import ( # noqa: PLC0415
TurboJPEGSingleton,
)
from homeassistant.components.camera import TurboJPEGSingleton # noqa: PLC0415
self._packet: Packet | None = None
self._event: asyncio.Event = asyncio.Event()

View File

@@ -7,7 +7,7 @@ import io
import logging
from ssl import SSLContext
from types import MappingProxyType
from typing import Any
from typing import Any, cast
import httpx
from telegram import (
@@ -23,6 +23,7 @@ from telegram import (
InputMediaVideo,
InputPollOption,
Message,
PhotoSize,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
Update,
@@ -56,6 +57,10 @@ from .const import (
ATTR_DISABLE_NOTIF,
ATTR_DISABLE_WEB_PREV,
ATTR_FILE,
ATTR_FILE_ID,
ATTR_FILE_MIME_TYPE,
ATTR_FILE_NAME,
ATTR_FILE_SIZE,
ATTR_FROM_FIRST,
ATTR_FROM_LAST,
ATTR_INLINE_MESSAGE_ID,
@@ -86,6 +91,7 @@ from .const import (
CONF_CHAT_ID,
CONF_PROXY_URL,
DOMAIN,
EVENT_TELEGRAM_ATTACHMENT,
EVENT_TELEGRAM_CALLBACK,
EVENT_TELEGRAM_COMMAND,
EVENT_TELEGRAM_SENT,
@@ -183,6 +189,10 @@ class BaseTelegramBot:
# This is a command message - set event type to command and split data into command and args
event_type = EVENT_TELEGRAM_COMMAND
event_data.update(self._get_command_event_data(message.text))
elif filters.ATTACHMENT.filter(message):
event_type = EVENT_TELEGRAM_ATTACHMENT
event_data[ATTR_TEXT] = message.caption
event_data.update(self._get_file_id_event_data(message))
else:
event_type = EVENT_TELEGRAM_TEXT
event_data[ATTR_TEXT] = message.text
@@ -192,6 +202,26 @@ class BaseTelegramBot:
return event_type, event_data
def _get_file_id_event_data(self, message: Message) -> dict[str, Any]:
"""Extract file_id from a message attachment, if any."""
if filters.PHOTO.filter(message):
photos = cast(Sequence[PhotoSize], message.effective_attachment)
return {
ATTR_FILE_ID: photos[-1].file_id,
ATTR_FILE_MIME_TYPE: "image/jpeg", # telegram always uses jpeg for photos
ATTR_FILE_SIZE: photos[-1].file_size,
}
return {
k: getattr(message.effective_attachment, v)
for k, v in (
(ATTR_FILE_ID, "file_id"),
(ATTR_FILE_NAME, "file_name"),
(ATTR_FILE_MIME_TYPE, "mime_type"),
(ATTR_FILE_SIZE, "file_size"),
)
if hasattr(message.effective_attachment, v)
}
def _get_user_event_data(self, user: User) -> dict[str, Any]:
return {
ATTR_USER_ID: user.id,
@@ -548,6 +578,7 @@ class TelegramNotificationService:
"Error sending message",
params[ATTR_MESSAGE_TAG],
text,
target=target,
parse_mode=params[ATTR_PARSER],
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
disable_notification=params[ATTR_DISABLE_NOTIF],

View File

@@ -54,6 +54,7 @@ SERVICE_LEAVE_CHAT = "leave_chat"
EVENT_TELEGRAM_CALLBACK = "telegram_callback"
EVENT_TELEGRAM_COMMAND = "telegram_command"
EVENT_TELEGRAM_TEXT = "telegram_text"
EVENT_TELEGRAM_ATTACHMENT = "telegram_attachment"
EVENT_TELEGRAM_SENT = "telegram_sent"
PARSER_HTML = "html"
@@ -90,6 +91,10 @@ ATTR_DISABLE_NOTIF = "disable_notification"
ATTR_DISABLE_WEB_PREV = "disable_web_page_preview"
ATTR_EDITED_MSG = "edited_message"
ATTR_FILE = "file"
ATTR_FILE_ID = "file_id"
ATTR_FILE_MIME_TYPE = "file_mime_type"
ATTR_FILE_NAME = "file_name"
ATTR_FILE_SIZE = "file_size"
ATTR_FROM_FIRST = "from_first"
ATTR_FROM_LAST = "from_last"
ATTR_KEYBOARD = "keyboard"

View File

@@ -21,7 +21,9 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.components.sensor.helpers import ( # pylint: disable=hass-component-root-import
async_parse_date_datetime,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,

View File

@@ -11,6 +11,6 @@
"tf-models-official==2.5.0",
"pycocotools==2.0.6",
"numpy==2.3.2",
"Pillow==11.3.0"
"Pillow==12.0.0"
]
}

View File

@@ -254,10 +254,11 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
def _is_position_reversed(self) -> bool:
"""Check if the cover position and direction should be reversed."""
# The default is True
# Having motor_reverse_mode == "back" cancels the inversion
# Having motor_reverse_mode == "forward" cancels the inversion
return not (
self._motor_reverse_mode_enum
and self.device.status.get(self._motor_reverse_mode_enum.dpcode) == "back"
and self.device.status.get(self._motor_reverse_mode_enum.dpcode)
== "forward"
)
@property

View File

@@ -20,7 +20,9 @@ from homeassistant.components.sensor import (
SensorExtraStoredData,
SensorStateClass,
)
from homeassistant.components.sensor.recorder import _suggest_report_issue
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
_suggest_report_issue,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/vesync",
"iot_class": "cloud_polling",
"loggers": ["pyvesync"],
"requirements": ["pyvesync==3.1.0"]
"requirements": ["pyvesync==3.1.2"]
}

View File

@@ -24,6 +24,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -117,6 +118,32 @@ GLOBAL_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = (
device_class=BinarySensorDeviceClass.PROBLEM,
value_getter=lambda api: len(api.getDeviceErrors()) > 0,
),
ViCareBinarySensorEntityDescription(
key="identification_mode",
translation_key="identification_mode",
entity_category=EntityCategory.DIAGNOSTIC,
value_getter=lambda api: api.getIdentification(),
entity_registry_enabled_default=False,
),
ViCareBinarySensorEntityDescription(
key="mounting_mode",
translation_key="mounting_mode",
entity_category=EntityCategory.DIAGNOSTIC,
value_getter=lambda api: api.getMountingMode(),
entity_registry_enabled_default=False,
),
ViCareBinarySensorEntityDescription(
key="child_safety_lock_mode",
translation_key="child_safety_lock_mode",
value_getter=lambda api: api.getChildLock() == "active",
entity_registry_enabled_default=False,
),
ViCareBinarySensorEntityDescription(
key="valve",
translation_key="valve",
device_class=BinarySensorDeviceClass.DOOR,
value_getter=lambda api: api.isValveOpen(),
),
)

View File

@@ -21,6 +21,15 @@
},
"one_time_charge": {
"default": "mdi:shower-head"
},
"mounting_mode": {
"default": "mdi:wrench"
},
"child_safety_lock_mode": {
"default": "mdi:lock"
},
"valve": {
"default": "mdi:pipe-valve"
}
},
"button": {
@@ -90,6 +99,12 @@
},
"ventilation_level": {
"default": "mdi:fan"
},
"zigbee_signal_strength": {
"default": "mdi:wifi"
},
"valve_position": {
"default": "mdi:pipe-valve"
}
}
},

View File

@@ -943,6 +943,23 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
value_getter=lambda api: api.getBatteryLevel(),
),
ViCareSensorEntityDescription(
key="zigbee_signal_strength",
translation_key="zigbee_signal_strength",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
value_getter=lambda api: api.getZigbeeSignalStrength(),
entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="valve_position",
translation_key="valve_position",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_getter=lambda api: api.getValvePosition(),
entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="fuel_need",
translation_key="fuel_need",

View File

@@ -66,6 +66,18 @@
},
"one_time_charge": {
"name": "One-time charge"
},
"identification_mode": {
"name": "Identification mode"
},
"mounting_mode": {
"name": "Mounting mode"
},
"child_safety_lock_mode": {
"name": "Child safety lock"
},
"valve": {
"name": "Valve"
}
},
"button": {
@@ -502,6 +514,12 @@
},
"fuel_need": {
"name": "Fuel need"
},
"zigbee_signal_strength": {
"name": "[%key:component::sensor::entity_component::signal_strength::name%]"
},
"valve_position": {
"name": "Valve position"
}
},
"water_heater": {

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.assist_pipeline.repair_flows import (
from homeassistant.components.assist_pipeline.repair_flows import ( # pylint: disable=hass-component-root-import
AssistInProgressDeprecatedRepairFlow,
)
from homeassistant.components.repairs import RepairsFlow

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.components.assist_pipeline.select import (
from homeassistant.components.assist_pipeline import (
AssistPipelineSelect,
VadSensitivitySelect,
)

View File

@@ -348,6 +348,12 @@
"odometer": {
"default": "mdi:counter"
},
"service_warning": {
"default": "mdi:wrench-clock",
"state": {
"no_warning": "mdi:car-wrench"
}
},
"target_battery_charge_level": {
"default": "mdi:battery-medium"
},

View File

@@ -8,6 +8,6 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["volvocarsapi"],
"quality_scale": "silver",
"quality_scale": "platinum",
"requirements": ["volvocarsapi==0.4.3"]
}

View File

@@ -42,7 +42,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: |
@@ -52,12 +52,12 @@ rules:
comment: |
No discovery possible.
docs-data-update: done
docs-examples: todo
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
@@ -70,7 +70,10 @@ rules:
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: todo
repair-issues:
status: exempt
comment: |
This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: |

View File

@@ -332,6 +332,25 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
),
# diagnostics endpoint
VolvoSensorDescription(
key="service_warning",
api_field="serviceWarning",
device_class=SensorDeviceClass.ENUM,
options=[
"distance_driven_almost_time_for_service",
"distance_driven_overdue_for_service",
"distance_driven_time_for_service",
"engine_hours_almost_time_for_service",
"engine_hours_overdue_for_service",
"engine_hours_time_for_service",
"no_warning",
"regular_maintenance_almost_time_for_service",
"regular_maintenance_overdue_for_service",
"regular_maintenance_time_for_service",
"unknown_warning",
],
),
# energy state endpoint
VolvoSensorDescription(
key="target_battery_charge_level",

View File

@@ -309,6 +309,22 @@
"odometer": {
"name": "Odometer"
},
"service_warning": {
"name": "Service",
"state": {
"distance_driven_almost_time_for_service": "Almost time for distance service",
"distance_driven_overdue_for_service": "Distance service overdue",
"distance_driven_time_for_service": "Time for distance service",
"engine_hours_almost_time_for_service": "Almost time for engine service",
"engine_hours_overdue_for_service": "Engine service overdue",
"engine_hours_time_for_service": "Time for engine service",
"no_warning": "No warning",
"regular_maintenance_almost_time_for_service": "Almost time for service",
"regular_maintenance_overdue_for_service": "Service overdue",
"regular_maintenance_time_for_service": "Time for service",
"unknown_warning": "Unknown warning"
}
},
"target_battery_charge_level": {
"name": "Target battery charge level"
},

View File

@@ -2,32 +2,169 @@
from __future__ import annotations
from types import MappingProxyType
from typing import TYPE_CHECKING
from aiowaqi import WAQIClient
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_STATION_NUMBER, DOMAIN, SUBENTRY_TYPE_STATION
from .coordinator import WAQIConfigEntry, WAQIDataUpdateCoordinator
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up WAQI."""
await async_migrate_integration(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool:
"""Set up World Air Quality Index (WAQI) from a config entry."""
client = WAQIClient(session=async_get_clientsession(hass))
client.authenticate(entry.data[CONF_API_KEY])
waqi_coordinator = WAQIDataUpdateCoordinator(hass, entry, client)
await waqi_coordinator.async_config_entry_first_refresh()
entry.runtime_data = waqi_coordinator
entry.runtime_data = {}
for subentry in entry.subentries.values():
if subentry.subentry_type != SUBENTRY_TYPE_STATION:
continue
# Create a coordinator for each station subentry
coordinator = WAQIDataUpdateCoordinator(hass, entry, subentry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data[subentry.subentry_id] = coordinator
entry.async_on_unload(entry.add_update_listener(async_update_entry))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_update_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> None:
"""Update entry."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure to subentries."""
# Make sure we get enabled config entries first
entries = sorted(
hass.config_entries.async_entries(DOMAIN),
key=lambda e: e.disabled_by is not None,
)
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
for entry in entries:
subentry = ConfigSubentry(
data=MappingProxyType(
{CONF_STATION_NUMBER: entry.data[CONF_STATION_NUMBER]}
),
subentry_type="station",
title=entry.title,
unique_id=entry.unique_id,
)
if entry.data[CONF_API_KEY] not in api_keys_entries:
all_disabled = all(
e.disabled_by is not None
for e in entries
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
)
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
hass.config_entries.async_add_subentry(parent_entry, subentry)
entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
if TYPE_CHECKING:
assert entry.unique_id is not None
device = device_registry.async_get_device(
identifiers={(DOMAIN, entry.unique_id)}
)
for entity_entry in entities:
entity_disabled_by = entity_entry.disabled_by
if (
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
# Device and entity registries don't update the disabled_by flag
# when moving a device or entity from one config entry to another,
# so we need to do it manually.
entity_disabled_by = (
er.RegistryEntryDisabler.DEVICE
if device
else er.RegistryEntryDisabler.USER
)
entity_registry.async_update_entity(
entity_entry.entity_id,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
disabled_by=entity_disabled_by,
)
if device is not None:
# Device and entity registries don't update the disabled_by flag when
# moving a device or entity from one config entry to another, so we
# need to do it manually.
device_disabled_by = device.disabled_by
if (
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
device_disabled_by = dr.DeviceEntryDisabler.USER
device_registry.async_update_device(
device.id,
disabled_by=device_disabled_by,
add_config_subentry_id=subentry.subentry_id,
add_config_entry_id=parent_entry.entry_id,
)
if parent_entry.entry_id != entry.entry_id:
device_registry.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
)
else:
device_registry.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
remove_config_subentry_id=None,
)
if parent_entry.entry_id != entry.entry_id:
await hass.config_entries.async_remove(entry.entry_id)
else:
hass.config_entries.async_update_entry(
entry,
title="WAQI",
version=2,
data={CONF_API_KEY: entry.data[CONF_API_KEY]},
unique_id=None,
)

View File

@@ -13,22 +13,24 @@ from aiowaqi import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
ConfigSubentryFlow,
SubentryFlowResult,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
CONF_METHOD,
)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
LocationSelector,
SelectSelector,
SelectSelectorConfig,
)
from homeassistant.helpers.selector import LocationSelector
from .const import CONF_STATION_NUMBER, DOMAIN
from .const import CONF_STATION_NUMBER, DOMAIN, SUBENTRY_TYPE_STATION
_LOGGER = logging.getLogger(__name__)
@@ -54,11 +56,15 @@ async def get_by_station_number(
class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for World Air Quality Index (WAQI)."""
VERSION = 1
VERSION = 2
def __init__(self) -> None:
"""Initialize config flow."""
self.data: dict[str, Any] = {}
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: ConfigEntry
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this handler."""
return {SUBENTRY_TYPE_STATION: StationFlowHandler}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -66,6 +72,7 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
client = WAQIClient(session=async_get_clientsession(self.hass))
client.authenticate(user_input[CONF_API_KEY])
try:
@@ -78,35 +85,40 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self.data = user_input
if user_input[CONF_METHOD] == CONF_MAP:
return await self.async_step_map()
return await self.async_step_station_number()
return self.async_create_entry(
title="World Air Quality Index",
data={
CONF_API_KEY: user_input[CONF_API_KEY],
},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_METHOD): SelectSelector(
SelectSelectorConfig(
options=[CONF_MAP, CONF_STATION_NUMBER],
translation_key="method",
)
),
}
),
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)
class StationFlowHandler(ConfigSubentryFlow):
"""Handle subentry flow."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""User flow to create a sensor subentry."""
return self.async_show_menu(
step_id="user",
menu_options=["map", "station_number"],
)
async def async_step_map(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
) -> SubentryFlowResult:
"""Add measuring station via map."""
errors: dict[str, str] = {}
if user_input is not None:
client = WAQIClient(session=async_get_clientsession(self.hass))
client.authenticate(self.data[CONF_API_KEY])
client.authenticate(self._get_entry().data[CONF_API_KEY])
try:
measuring_station = await client.get_by_coordinates(
user_input[CONF_LOCATION][CONF_LATITUDE],
@@ -124,9 +136,7 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(
CONF_LOCATION,
): LocationSelector(),
vol.Required(CONF_LOCATION): LocationSelector(),
}
),
{
@@ -141,12 +151,12 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_station_number(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
) -> SubentryFlowResult:
"""Add measuring station via station number."""
errors: dict[str, str] = {}
if user_input is not None:
client = WAQIClient(session=async_get_clientsession(self.hass))
client.authenticate(self.data[CONF_API_KEY])
client.authenticate(self._get_entry().data[CONF_API_KEY])
station_number = user_input[CONF_STATION_NUMBER]
measuring_station, errors = await get_by_station_number(
client, abs(station_number)
@@ -160,25 +170,22 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
return await self._async_create_entry(measuring_station)
return self.async_show_form(
step_id=CONF_STATION_NUMBER,
data_schema=vol.Schema(
{
vol.Required(
CONF_STATION_NUMBER,
): int,
}
),
data_schema=vol.Schema({vol.Required(CONF_STATION_NUMBER): int}),
errors=errors,
)
async def _async_create_entry(
self, measuring_station: WAQIAirQuality
) -> ConfigFlowResult:
await self.async_set_unique_id(str(measuring_station.station_id))
self._abort_if_unique_id_configured()
) -> SubentryFlowResult:
station_id = str(measuring_station.station_id)
for entry in self.hass.config_entries.async_entries(DOMAIN):
for subentry in entry.subentries.values():
if subentry.unique_id == station_id:
return self.async_abort(reason="already_configured")
return self.async_create_entry(
title=measuring_station.city.name,
data={
CONF_API_KEY: self.data[CONF_API_KEY],
CONF_STATION_NUMBER: measuring_station.station_id,
},
unique_id=station_id,
)

View File

@@ -8,4 +8,4 @@ LOGGER = logging.getLogger(__package__)
CONF_STATION_NUMBER = "station_number"
ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=waqi"}
SUBENTRY_TYPE_STATION = "station"

View File

@@ -6,13 +6,13 @@ from datetime import timedelta
from aiowaqi import WAQIAirQuality, WAQIClient, WAQIError
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_STATION_NUMBER, DOMAIN, LOGGER
from .const import CONF_STATION_NUMBER, LOGGER
type WAQIConfigEntry = ConfigEntry[WAQIDataUpdateCoordinator]
type WAQIConfigEntry = ConfigEntry[dict[str, WAQIDataUpdateCoordinator]]
class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]):
@@ -21,22 +21,27 @@ class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]):
config_entry: WAQIConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: WAQIConfigEntry, client: WAQIClient
self,
hass: HomeAssistant,
config_entry: WAQIConfigEntry,
subentry: ConfigSubentry,
client: WAQIClient,
) -> None:
"""Initialize the WAQI data coordinator."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name=DOMAIN,
name=subentry.title,
update_interval=timedelta(minutes=5),
)
self._client = client
self.subentry = subentry
async def _async_update_data(self) -> WAQIAirQuality:
try:
return await self._client.get_by_station_number(
self.config_entry.data[CONF_STATION_NUMBER]
self.subentry.data[CONF_STATION_NUMBER]
)
except WAQIError as exc:
raise UpdateFailed from exc

View File

@@ -130,12 +130,15 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the WAQI sensor."""
coordinator = entry.runtime_data
async_add_entities(
WaqiSensor(coordinator, sensor)
for sensor in SENSORS
if sensor.available_fn(coordinator.data)
)
for subentry_id, coordinator in entry.runtime_data.items():
async_add_entities(
(
WaqiSensor(coordinator, sensor)
for sensor in SENSORS
if sensor.available_fn(coordinator.data)
),
config_subentry_id=subentry_id,
)
class WaqiSensor(CoordinatorEntity[WAQIDataUpdateCoordinator], SensorEntity):

View File

@@ -3,19 +3,10 @@
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"method": "How do you want to select a measuring station?"
}
},
"map": {
"description": "Select a location to get the closest measuring station.",
"data": {
"location": "[%key:common::config_flow::data::location%]"
}
},
"station_number": {
"data": {
"station_number": "Measuring station number"
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "API key for the World Air Quality Index"
}
}
},
@@ -25,15 +16,44 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"selector": {
"method": {
"options": {
"map": "Select nearest from point on the map",
"station_number": "Enter a station number"
}
"config_subentries": {
"station": {
"step": {
"user": {
"title": "Add measuring station",
"description": "How do you want to select a measuring station?",
"menu_options": {
"map": "[%key:common::config_flow::data::location%]",
"station_number": "Measuring station number"
}
},
"map": {
"data": {
"location": "[%key:common::config_flow::data::location%]"
},
"data_description": {
"location": "The location to get the nearest measuring station from"
}
},
"station_number": {
"data": {
"station_number": "[%key:component::waqi::config_subentries::station::step::user::menu_options::station_number%]"
},
"data_description": {
"station_number": "The number of the measuring station"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"initiate_flow": {
"user": "Add measuring station"
},
"entry_type": "Measuring station"
}
},
"entity": {

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.assist_pipeline.vad import VadSensitivity
from homeassistant.components.assist_pipeline import VadSensitivity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er

View File

@@ -4,11 +4,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Final
from homeassistant.components.assist_pipeline.select import (
from homeassistant.components.assist_pipeline import (
AssistPipelineSelect,
VadSensitivity,
VadSensitivitySelect,
)
from homeassistant.components.assist_pipeline.vad import VadSensitivity
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory

View File

@@ -8,14 +8,13 @@ from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.smartglass.models import SmartglassConsoleList
from xbox.webapi.common.signed_session import SignedSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from . import api
from .const import DOMAIN
from .coordinator import XboxUpdateCoordinator
from .coordinator import XboxConfigEntry, XboxUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -29,7 +28,7 @@ PLATFORMS = [
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool:
"""Set up xbox from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
@@ -45,30 +44,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug(
"Found %d consoles: %s",
len(consoles.result),
consoles.dict(),
consoles.model_dump(),
)
coordinator = XboxUpdateCoordinator(hass, entry, client, consoles)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"client": XboxLiveClient(auth),
"consoles": consoles,
"coordinator": 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: XboxConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
# Unsub from coordinator updates
hass.data[DOMAIN][entry.entry_id]["sensor_unsub"]()
hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"]()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -5,13 +5,11 @@ from __future__ import annotations
from functools import partial
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import XboxUpdateCoordinator
from .coordinator import XboxConfigEntry, XboxUpdateCoordinator
from .entity import XboxBaseEntity
PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"]
@@ -19,18 +17,16 @@ PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: XboxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Xbox Live friends."""
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
"coordinator"
]
coordinator = entry.runtime_data
update_friends = partial(async_update_friends, coordinator, {}, async_add_entities)
unsub = coordinator.async_add_listener(update_friends)
hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"] = unsub
entry.async_on_unload(coordinator.async_add_listener(update_friends))
update_friends()

View File

@@ -28,6 +28,8 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type XboxConfigEntry = ConfigEntry[XboxUpdateCoordinator]
@dataclass
class ConsoleData:

View File

@@ -5,13 +5,11 @@ from __future__ import annotations
import re
from typing import Any
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.models import Image
from xbox.webapi.api.provider.smartglass.models import (
PlaybackState,
PowerState,
SmartglassConsole,
SmartglassConsoleList,
VolumeDirection,
)
@@ -21,7 +19,6 @@ from homeassistant.components.media_player import (
MediaPlayerState,
MediaType,
)
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
@@ -29,7 +26,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .browse_media import build_item_response
from .const import DOMAIN
from .coordinator import ConsoleData, XboxUpdateCoordinator
from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator
SUPPORT_XBOX = (
MediaPlayerEntityFeature.TURN_ON
@@ -57,18 +54,18 @@ XBOX_STATE_MAP: dict[PlaybackState | PowerState, MediaPlayerState | None] = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: XboxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Xbox media_player from a config entry."""
client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]["client"]
consoles: SmartglassConsoleList = hass.data[DOMAIN][entry.entry_id]["consoles"]
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
"coordinator"
]
coordinator = entry.runtime_data
async_add_entities(
[XboxMediaPlayer(client, console, coordinator) for console in consoles.result]
[
XboxMediaPlayer(console, coordinator)
for console in coordinator.consoles.result
]
)
@@ -77,14 +74,13 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
def __init__(
self,
client: XboxLiveClient,
console: SmartglassConsole,
coordinator: XboxUpdateCoordinator,
) -> None:
"""Initialize the Xbox Media Player."""
super().__init__(coordinator)
self.client: XboxLiveClient = client
self._console: SmartglassConsole = console
self.client = coordinator.client
self._console = console
@property
def name(self):

View File

@@ -24,6 +24,7 @@ from homeassistant.util import dt as dt_util
from .browse_media import _find_media_image
from .const import DOMAIN
from .coordinator import XboxConfigEntry
MIME_TYPE_MAP = {
"gameclips": "video/mp4",
@@ -38,8 +39,8 @@ MEDIA_CLASS_MAP = {
async def async_get_media_source(hass: HomeAssistant):
"""Set up Xbox media source."""
entry = hass.config_entries.async_entries(DOMAIN)[0]
client = hass.data[DOMAIN][entry.entry_id]["client"]
entry: XboxConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
client = entry.runtime_data.client
return XboxSource(hass, client)

View File

@@ -7,12 +7,10 @@ from collections.abc import Iterable
import re
from typing import Any
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.smartglass.models import (
InputKeyType,
PowerState,
SmartglassConsole,
SmartglassConsoleList,
)
from homeassistant.components.remote import (
@@ -21,30 +19,25 @@ from homeassistant.components.remote import (
DEFAULT_DELAY_SECS,
RemoteEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ConsoleData, XboxUpdateCoordinator
from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: XboxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Xbox media_player from a config entry."""
client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]["client"]
consoles: SmartglassConsoleList = hass.data[DOMAIN][entry.entry_id]["consoles"]
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
"coordinator"
]
coordinator = entry.runtime_data
async_add_entities(
[XboxRemote(client, console, coordinator) for console in consoles.result]
[XboxRemote(console, coordinator) for console in coordinator.consoles.result]
)
@@ -53,14 +46,13 @@ class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity):
def __init__(
self,
client: XboxLiveClient,
console: SmartglassConsole,
coordinator: XboxUpdateCoordinator,
) -> None:
"""Initialize the Xbox Media Player."""
super().__init__(coordinator)
self.client: XboxLiveClient = client
self._console: SmartglassConsole = console
self.client = coordinator.client
self._console = console
@property
def name(self):

View File

@@ -5,13 +5,11 @@ from __future__ import annotations
from functools import partial
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import XboxUpdateCoordinator
from .coordinator import XboxConfigEntry, XboxUpdateCoordinator
from .entity import XboxBaseEntity
SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"]
@@ -19,18 +17,15 @@ SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: XboxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Xbox Live friends."""
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][
"coordinator"
]
coordinator = config_entry.runtime_data
update_friends = partial(async_update_friends, coordinator, {}, async_add_entities)
unsub = coordinator.async_add_listener(update_friends)
hass.data[DOMAIN][config_entry.entry_id]["sensor_unsub"] = unsub
config_entry.async_on_unload(coordinator.async_add_listener(update_friends))
update_friends()

View File

@@ -58,7 +58,7 @@ ACTION_PARAMETERS_CACHE: HassKey[
LLM_API_ASSIST = "assist"
BASE_PROMPT = (
DATE_TIME_PROMPT = (
'Current time is {{ now().strftime("%H:%M:%S") }}. '
'Today\'s date is {{ now().strftime("%Y-%m-%d") }}.\n'
)
@@ -592,6 +592,8 @@ class AssistAPI(API):
for intent_handler in intent_handlers
]
tools.append(GetDateTimeTool())
if exposed_entities:
if exposed_entities[CALENDAR_DOMAIN]:
names = []
@@ -1181,3 +1183,29 @@ class GetLiveContextTool(Tool):
"success": True,
"result": "\n".join(prompt),
}
class GetDateTimeTool(Tool):
"""Tool for getting the current date and time."""
name = "GetDateTime"
description = "Provides the current date and time."
async def async_call(
self,
hass: HomeAssistant,
tool_input: ToolInput,
llm_context: LLMContext,
) -> JsonObjectType:
"""Get the current date and time."""
now = dt_util.now()
return {
"success": True,
"result": {
"date": now.strftime("%Y-%m-%d"),
"time": now.strftime("%H:%M:%S"),
"timezone": now.strftime("%Z"),
"weekday": now.strftime("%A"),
},
}

Some files were not shown because too many files have changed in this diff Show More