Compare commits

..

90 Commits

Author SHA1 Message Date
G Johansson 3d248d9f2f Add test 2025-05-31 18:37:28 +00:00
G Johansson 3c201548ab Adjust ManualTriggerSensorEntity to handle timestap device classes 2025-05-30 20:25:41 +00:00
J. Diego Rodríguez Royo 1e973c1d74 Bump aiohomeconnect to 0.17.1 (#145873) 2025-05-30 01:40:11 +02:00
starkillerOG 618ada64f8 Ensure Reolink host device is setup first (#145843) 2025-05-29 19:32:21 +02:00
Robert Resch 2d6802e06a Deprecate tensorflow (#145806)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-29 14:35:35 +01:00
starkillerOG 9687a34a70 Reolink fallback to download command for playback (#145842) 2025-05-29 15:31:50 +02:00
Michael 5ba0ceb6c2 Bump aioimmich to 0.7.0 (#145845) 2025-05-29 15:30:02 +02:00
G Johansson d8e3e88c63 Fix language selections in workday (#145813) 2025-05-29 15:28:54 +02:00
Robert Resch d1d1bca29d Deprecate sms integration (#145847) 2025-05-29 14:12:51 +02:00
Michael 80189495c5 Use mime type provided by Immich (#145830)
use mime type from immich instead of guessing it
2025-05-29 10:28:02 +02:00
Josef Zweck cad6c72cfa Bump aiotedee to 0.2.23 (#145822)
* Bump aiotedee to 0.2.23

* update snapshot
2025-05-29 10:35:05 +03:00
J. Nick Koston 23ac22e213 Remove default args to ESPHome test fixture calls (#145840) 2025-05-29 01:45:37 -05:00
J. Nick Koston 55e664fc0d Bump aiohttp to 3.12.4 (#145838) 2025-05-28 21:08:01 -05:00
Brett Adams 881ce45afa Fix Tessie volume max and step (#145835)
* Use fixed volume max and step

* Update snapshot
2025-05-29 03:58:29 +02:00
André Lersveen b80195df81 Set correct nobo_hub max temperature (#145751)
Max temperature 30°C is implemented upstream in pynobo and the Nobø Energy Hub app also stops at 30°C.
2025-05-29 03:52:05 +02:00
Matthew FitzGerald-Chamberlain e57ce0a9df Bump pyaprilaire to 0.9.1 (#145836) 2025-05-29 03:43:28 +02:00
J. Nick Koston ff66ad7705 Bump aiohttp to 3.12.3 (#145837) 2025-05-28 19:38:06 -05:00
Robert Resch 33e98ebffa Remove decora-wifi from excluded requirements (#145832) 2025-05-29 00:14:38 +02:00
Robert Resch 8fd9e2046e Deprecate decora integration (#145807) 2025-05-28 23:54:48 +02:00
Bram Kragten 32c2f47ab5 Update frontend to 20250528.0 (#145828)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-05-28 23:17:14 +02:00
Ståle Storø Hauknes e2fc2dce84 Move Airthings coordinator to separate module (#145827)
* Create coordinator

* Fix sensor.py
2025-05-28 22:38:33 +02:00
Michael afa97f8ec1 Add level of collections in Immich media source tree (#145734)
* add layer for collections in media source tree

* re-arange tests, add test for collection layer

* fix
2025-05-28 20:51:27 +02:00
Michael 2708c1c94c Fix Immich media source browsing with multiple config entries (#145823)
fix media source browsing with multiple config entries
2025-05-28 20:49:20 +02:00
Michael Hansen d76ed6a3c2 Bump intents to 2025.5.28 (#145816) 2025-05-28 21:14:13 +03:00
epenet 695f69bd90 Remove unnecessary DOMAIN alias in tests (e-k) (#145818) 2025-05-28 21:06:25 +03:00
epenet 7da8e24e21 Remove unnecessary DOMAIN alias in tests (a-d) (#145817) 2025-05-28 21:00:38 +03:00
David Bonnes 9d0fc0d513 Fix HOMEASSISTANT_STOP unsubscribe in data update coordinator (#145809)
* initial commit

* a better approach

* Add comment
2025-05-28 17:52:51 +01:00
Robert Resch ca567aa7fc Deprecate lirc integration (#145797) 2025-05-28 17:28:37 +01:00
Robert Resch 27af2d8ec6 Deprecate keyboard integration (#145805) 2025-05-28 17:22:18 +02:00
Lennart Nederstigt 59ea6f375a Add hardwired chime toggle to Reolink Battery Doorbell (#145779)
Co-authored-by: starkillerOG <starkiller.og@gmail.com>
2025-05-28 17:10:38 +02:00
Marc Mueller 6c365c94ed Update sqlalchemy to 2.0.41 (#145790) 2025-05-28 16:39:10 +02:00
Marc Mueller 6693fc764f Update httpcore to 1.0.9 and h11 to 0.16.0 (#145789) 2025-05-28 16:35:11 +02:00
starkillerOG e855b6c2bc Bump reolink-aio to 0.13.4 (#145799) 2025-05-28 16:33:20 +02:00
Abílio Costa 23a1dddc23 Add Shelly zwave virtual integration (#145749) 2025-05-28 14:56:47 +01:00
epenet bd5fef1ddb Use async_load_fixture in async test functions (a) (#145718) 2025-05-28 15:51:49 +02:00
epenet c3ade400fb Use Platform constant in tests (#145801)
* Use Platform constant in tests

* spelling

* Fix platform
2025-05-28 15:51:37 +02:00
epenet 1889f0ef66 Use Platform constant in hue tests (#145798) 2025-05-28 14:43:48 +02:00
epenet 6b28af8282 Remove unnecessary DOMAIN alias in components (#145791) 2025-05-28 14:04:35 +02:00
Robert Resch f59001d45f Deprecate pandora integration (#145785) 2025-05-28 13:12:55 +02:00
Erik Montnemery a857461059 Handle late abort when creating subentry (#145765)
* Handle late abort when creating subentry

* Move error handling to the base class

* Narrow down expected error in test
2025-05-28 12:26:28 +02:00
epenet e4cc842584 Use async_load_json_(array/object)_fixture in async test functions (#145773) 2025-05-28 12:09:05 +02:00
Robert Resch bb52058920 Deprecate GStreamer integration (#145768) 2025-05-28 11:16:08 +02:00
J. Diego Rodríguez Royo c1676570da Add more information about possible hostnames at Home Connect (#145770) 2025-05-28 10:57:01 +02:00
G Johansson 4858b2171e Modernize tests for smhi (#139334)
* Modernize tests for smhi

* Fixes

* Mods

* Fix weather

* Coverage 100%

* Fix init test

* Fixes

* Fixes

* Remove waits
2025-05-28 10:56:07 +02:00
Jan Bouwhuis 192aa76cd7 Ensure mqtt sensor unit of measurement validation for state class measurement_angle (#145648) 2025-05-28 10:16:40 +02:00
Josef Zweck ddf611bfdf Fix uom for prebrew numbers in lamarzocco (#145772) 2025-05-28 10:15:24 +02:00
Robert Resch 3164394982 Deprecate dlib image processing integrations (#145767) 2025-05-28 09:58:44 +02:00
Josef Zweck b250a03ff5 Bump pylamarzocco to 2.0.7 (#145763) 2025-05-28 09:39:33 +02:00
dependabot[bot] 2dd7f035f6 Bump docker/build-push-action from 6.17.0 to 6.18.0 (#145764)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 09:10:37 +02:00
Joost Lekkerkerker 2c08b3f30c Add more Amazon Devices DHCP matches (#145754) 2025-05-28 08:43:59 +02:00
Josef Zweck c3ec30ce3b Update otp description for amazon_devices (#145701)
* Update otp description from amazon_devices

* separate

* Update strings.json
2025-05-28 08:13:28 +02:00
Erik Montnemery 9d4375ca76 Make async_remove_stale_devices_links_keep_entity_device move entities (#145719)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-27 23:00:52 +02:00
Raphael Hehl 3870b87db9 Bump uiprotect to version 7.10.1 (#145737)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-05-27 22:58:46 +02:00
Joost Lekkerkerker ff2fd7e9ef Add DHCP discovery to LG ThinQ (#145746) 2025-05-27 16:45:30 -04:00
G Johansson 719dd09eb3 Fix dns resolver error in dnsip config flow validation (#145735)
Fix dns resolver error in dnsip
2025-05-27 22:17:34 +02:00
Bram Kragten 2cf2613dbd Update frontend to 20250527.0 (#145741) 2025-05-27 22:12:07 +02:00
Jan Bouwhuis 181a3d142e Revert "squeezebox Better result for testing (#144622)" (#145739)
This reverts commit 987af8f7df.
2025-05-27 21:36:51 +02:00
Elias Wernicke c20ad5fde1 Add complete intent function for shopping list component (#128565)
* add intent

* add tests

* raise IntentHandleError

* add check for non completed

* Prefer completing non complete items

* cleanup

* cleanup tests

* rename test

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* remove duplicated test

* update test

* complete all items

* fix event

* remove type def

* return speech slots

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-27 14:35:14 -05:00
Erwin Douna 4fcebf18dc Tado update mobile devices interval (#145738)
Update the mobile devices interval to five minutes
2025-05-27 21:27:52 +02:00
Joost Lekkerkerker a6e04be076 Remove niko_home_control YAML import (#145732) 2025-05-27 19:58:05 +02:00
Erwin Douna 330a8e197d MELCloud remove deprecated YAML import strings (#145731)
Remove deprecated YAML import strings
2025-05-27 19:50:31 +02:00
Joost Lekkerkerker 4300e846e6 Fix unbound local variable in Acmeda config flow (#145729) 2025-05-27 19:29:04 +02:00
Kevin Stillhammer 07fd1f99df Support addresses with comma in google_travel_time (#145663)
Support addresses with comma
2025-05-27 18:53:45 +02:00
Kevin Stillhammer 481639bcf9 Catch PermissionDenied(Route API disabled) in google_travel_time (#145722)
Catch PermissionDenied(Route API disabled)
2025-05-27 18:45:49 +02:00
Martin Hjelmare 376008940b Disable advanced window cover position Matter sensor by default (#145713)
* Disable advanced window cover position Matter sensor by default

* Enanble disabled sensors in snapshot test
2025-05-27 17:46:21 +02:00
epenet b2c2db3394 Add check for transient packages restricting Python version (#145695) 2025-05-27 17:45:51 +02:00
Kevin Stillhammer a636e38d24 Debug log the update response in google_travel_time (#145725)
Debug log the update response
2025-05-27 17:44:48 +02:00
Martin Hjelmare ae1294830c Remove static pin code length Matter sensors (#145711)
* Remove static Matter sensors

* Clean up translation strings
2025-05-27 17:35:11 +02:00
Robin Lintermann d87fdf028b Improve smarla base entity (#145710) 2025-05-27 15:58:19 +02:00
Petar Petrov 6f5d5d4cdb Change text of installing and starting Z-WaveJs add-on steps (#145702) 2025-05-27 14:51:22 +02:00
epenet 12fdd7034a Simplify boolean check in onewire (#145700) 2025-05-27 13:30:44 +02:00
Martin Hjelmare f295d72cd9 Fix error stack trace for HomeAssistantError in websocket service call (#145699)
* Add test

* Fix error stack trace for HomeAssistantError in websocket service call
2025-05-27 12:54:57 +02:00
Petar Petrov 2605fda185 Remove confirm screen after Z-Wave usb discovery (#145682)
* Remove confirm screen after Z-Wave usb discovery

* Simplify async_step_usb
2025-05-27 12:53:30 +02:00
Joost Lekkerkerker 2189dc3e2a Use string type for amazon devices OTP code (#145698) 2025-05-27 12:33:02 +02:00
Franck Nijhof 8364d8a2e3 Bump version to 2025.7.0dev0 (#145647)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-05-27 10:59:34 +02:00
epenet 96c9636086 Add check for packages restricting Python version (#145690)
* Add check for packages restricting Python version

* Apply suggestions from code review

* until

* until
2025-05-27 10:44:00 +02:00
Petar Petrov 7b1dfc35d1 Change description on recommended/custom Z-Wave install step (#145688)
Change description on recommended/custom Z-WaveJS step
2025-05-27 10:04:29 +02:00
Norbert Rittel 2e94730491 Replace "Invalid API key" with common string in overseerr (#145689)
Replace "Invalid API key" with common string
2025-05-27 09:56:16 +02:00
Markus Adrario 11c6998bf2 Add homee siren platform (#145675)
* port siren.py from custom component

* Add Siren Tests

* last small nits
2025-05-27 09:48:59 +02:00
epenet 055a024d10 Add async-timeout to forbidden packages (#145679) 2025-05-27 08:57:35 +02:00
Joost Lekkerkerker f73afd71fd Fix Amazon devices offline handling (#145656) 2025-05-27 08:49:25 +02:00
Jan Bouwhuis ec64194ab9 Fix justnimbus CI test (#145681) 2025-05-27 08:48:06 +02:00
karwosts d49a613c62 Add read_only entity_id to Trend options flow (#145657) 2025-05-27 08:42:08 +02:00
Artur Pragacz 6fc064fa6a Test that recorder is not promoted to earlier stage in bootstrap (#142695)
Test that recorder is not promoted to earlier stage
2025-05-27 08:23:39 +02:00
Artur Pragacz b36b591ccf Improve error message for global timeout (#141563)
* Improve error message for global timeout

* Add test

* Message works with zone too
2025-05-27 07:49:18 +02:00
J. Nick Koston d25ba79427 Bump aiohttp to 3.12.2 (#145671) 2025-05-26 21:58:46 -05:00
Joost Lekkerkerker df35f30321 Handle Google Nest DHCP flows (#145658)
* Handle Google Nest DHCP flows

* Handle Google Nest DHCP flows
2025-05-26 15:01:35 -07:00
Jan Bouwhuis 1e3d06a993 Fix translation for sensor measurement angle state class (#145649) 2025-05-26 22:47:53 +01:00
Florian von Garrel 2ee6bf7340 Add update platform to paperless integration (#145638)
* Add uüdate platform to paperless integration

* Add tests to paperless

* Add translation

* Fixed update unavailable

* Fetch remote version in update platform

* changed diagnostics

* changed diagnostic data

* Code quality

* revert changes

* code quality
2025-05-26 23:24:53 +02:00
Joost Lekkerkerker 13a8e5e021 Fix Aquacell snapshot (#145651) 2025-05-26 23:08:07 +02:00
360 changed files with 4366 additions and 2391 deletions
+2 -2
View File
@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
+1 -1
View File
@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.6"
HA_SHORT_VERSION: "2025.7"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
+10 -4
View File
@@ -171,8 +171,6 @@ FRONTEND_INTEGRATIONS = {
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
# The substage containing recorder should have no timeout, as it could cancel a database migration.
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
# The substages preceding it should also have no timeout, until we ensure that the recorder
# is not accidentally promoted as a dependency of any of the integrations in them.
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
STAGE_0_INTEGRATIONS = (
# Load logging and http deps as soon as possible
@@ -929,7 +927,11 @@ async def _async_set_up_integrations(
await _async_setup_multi_components(hass, stage_all_domains, config)
continue
try:
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
async with hass.timeout.async_timeout(
timeout,
cool_down=COOLDOWN_TIME,
cancel_message=f"Bootstrap stage {name} timeout",
):
await _async_setup_multi_components(hass, stage_all_domains, config)
except TimeoutError:
_LOGGER.warning(
@@ -941,7 +943,11 @@ async def _async_set_up_integrations(
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
async with hass.timeout.async_timeout(
WRAP_UP_TIMEOUT,
cool_down=COOLDOWN_TIME,
cancel_message="Bootstrap startup wrap up timeout",
):
await hass.async_block_till_done()
except TimeoutError:
_LOGGER.warning(
+6
View File
@@ -0,0 +1,6 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}
@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries()
}
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError):
async with timeout(5):
hubs: list[aiopulse.Hub] = [
hubs = [
hub
async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured
+2 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
)
from . import AgentDVRConfigEntry
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
)
self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Agent",
model="Camera",
name=f"{device.client.name} {device.name}",
+5 -19
View File
@@ -5,23 +5,22 @@ from __future__ import annotations
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from airthings import Airthings
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SECRET, DOMAIN
from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
@@ -32,21 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
async def _update_method() -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
@@ -0,0 +1,36 @@
"""The Airthings integration."""
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,
)
self.airthings = airthings
async def _update_method(self) -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await self.airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
+4 -3
View File
@@ -27,8 +27,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
from . import AirthingsConfigEntry
from .const import DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator
SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription(
@@ -140,7 +141,7 @@ async def async_setup_entry(
class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
):
"""Representation of a Airthings Sensor device."""
@@ -149,7 +150,7 @@ class AirthingsHeaterEnergySensor(
def __init__(
self,
coordinator: AirthingsDataCoordinatorType,
coordinator: AirthingsDataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
) -> None:
@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.positive_int,
vol.Required(CONF_CODE): cv.string,
}
),
)
@@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._serial_num in self.coordinator.data
return (
super().available
and self._serial_num in self.coordinator.data
and self.device.online
)
@@ -13,6 +13,7 @@
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "74D637*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "901195*" },
@@ -22,7 +23,8 @@
{ "macaddress": "A8E621*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" }
{ "macaddress": "EC2BEB*" },
{ "macaddress": "F02F9E*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
@@ -5,7 +5,7 @@
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password sent to your email address."
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
},
"config": {
"flow_title": "{username}",
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.9.0"]
"requirements": ["pyaprilaire==0.9.1"]
}
+2 -2
View File
@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
from ..const import ATTR_MANUFACTURER, DOMAIN
from .config import AxisConfig
from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource
@@ -79,7 +79,7 @@ class AxisHub:
config_entry_id=self.config.entry.entry_id,
configuration_url=self.api.config.url,
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
identifiers={(AXIS_DOMAIN, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer=ATTR_MANUFACTURER,
model=f"{self.config.model} {self.product_type}",
name=self.config.name,
@@ -10,8 +10,6 @@ from typing import Any
from jsonpath import jsonpath
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_COMMAND,
CONF_NAME,
@@ -189,17 +187,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self.entity_id, variables, None
)
if self.device_class not in {
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
}:
self._attr_native_value = value
elif value is not None:
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._process_manual_data(variables)
self._set_value_with_possible_timestamp(value, self.device_class, variables)
self.async_write_ha_state()
async def async_update(self) -> None:
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
}
+3 -3
View File
@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from .const import CONF_GESTURE, DOMAIN
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
from .device_trigger import (
CONF_BOTH_BUTTONS,
@@ -200,6 +200,6 @@ def async_describe_events(
}
async_describe_event(
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
)
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
@@ -1 +1,3 @@
"""The decora component."""
DOMAIN = "decora"
+19
View File
@@ -21,7 +21,11 @@ from homeassistant.components.light import (
LightEntity,
)
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from . import DOMAIN
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
@@ -90,6 +94,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up an Decora switch."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Leviton Decora",
},
)
lights = []
for address, device_config in config[CONF_DEVICES].items():
device = {}
@@ -1 +1,3 @@
"""The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"
@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Detect",
},
)
source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
@@ -1 +1,4 @@
"""The dlib_face_identify component."""
CONF_FACES = "faces"
DOMAIN = "dlib_face_identify"
@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{
@@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Identify",
},
)
confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE]
@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import contextlib
from typing import Any
from typing import Any, Literal
import aiodns
from aiodns.error import DNSError
@@ -62,16 +62,16 @@ async def async_validate_hostname(
"""Validate hostname."""
async def async_check(
hostname: str, resolver: str, qtype: str, port: int = 53
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
) -> bool:
"""Return if able to resolve hostname."""
result = False
result: bool = False
with contextlib.suppress(DNSError):
result = bool(
await aiodns.DNSResolver( # type: ignore[call-overload]
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
_resolver = aiodns.DNSResolver(
nameservers=[resolver], udp_port=port, tcp_port=port
)
result = bool(await _resolver.query(hostname, qtype))
return result
result: dict[str, bool] = {}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250526.0"]
"requirements": ["home-assistant-frontend==20250528.0"]
}
@@ -50,7 +50,12 @@ from .const import (
UNITS_IMPERIAL,
UNITS_METRIC,
)
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema(
{
@@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION],
)
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException:
return {"base": "invalid_auth"}
except TimeoutError:
@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden,
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
from google.maps.routing_v2 import (
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try:
formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid):
except (AttributeError, vol.Invalid):
return Waypoint(address=location)
return Waypoint(
location=Location(
@@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)]
)
except PermissionDenied as permission_error:
_LOGGER.error("Permission denied: %s", permission_error.message)
raise PermissionDeniedException from permission_error
except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception):
"""Unknown API Error."""
class PermissionDeniedException(Exception):
"""Permission Denied Error."""
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Create an issue for the Routes API being disabled."""
async_create_issue(
hass,
DOMAIN,
f"routes_api_disabled_{entry.entry_id}",
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="routes_api_disabled",
translation_placeholders={
"entry_title": entry.title,
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
},
)
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Delete the issue for the Routes API being disabled."""
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")
@@ -7,7 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import GoogleAPIError
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
from google.maps.routing_v2 import (
ComputeRoutesRequest,
Route,
@@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM,
)
from .helpers import convert_to_waypoint
from .helpers import (
convert_to_waypoint,
create_routes_api_disabled_issue,
delete_routes_api_disabled_issue,
)
_LOGGER = logging.getLogger(__name__)
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
)
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0:
self._route = response.routes[0]
delete_routes_api_disabled_issue(self.hass, self._config_entry)
except PermissionDenied:
_LOGGER.error("Routes API is disabled for this API key")
create_routes_api_disabled_issue(self.hass, self._config_entry)
self._route = None
except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex)
self._route = None
@@ -21,6 +21,7 @@
}
},
"error": {
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers"
}
}
},
"issues": {
"routes_api_disabled": {
"title": "The Routes API must be enabled",
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
}
}
}
+5 -5
View File
@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
from .entity import GroupEntity
DEFAULT_NAME = "Sensor Group"
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return state_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_state_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return device_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_device_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
if device_class:
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_uoms_not_matching_device_class",
is_fixable=False,
is_persistent=False,
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
else:
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_uoms_not_matching_no_device_class",
is_fixable=False,
is_persistent=False,
@@ -1 +1,3 @@
"""The gstreamer component."""
DOMAIN = "gstreamer"
@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Gstreamer platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "GStreamer",
},
)
name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE)
@@ -10,17 +10,17 @@
"macaddress": "C8D778*"
},
{
"hostname": "(bosch|siemens)-*",
"hostname": "(balay|bosch|neff|siemens)-*",
"macaddress": "68A40E*"
},
{
"hostname": "siemens-*",
"hostname": "(siemens|neff)-*",
"macaddress": "38B4D3*"
}
],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.0"],
"requirements": ["aiohomeconnect==0.17.1"],
"zeroconf": ["_homeconnect._tcp.local."]
}
@@ -18,6 +18,10 @@
"title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"deprecated_system_packages_config_flow_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
},
"deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
@@ -27,6 +27,7 @@ PLATFORMS = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,
Platform.VALVE,
]
+49
View File
@@ -0,0 +1,49 @@
"""The homee siren platform."""
from typing import Any
from pyHomee.const import AttributeType
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Add siren entities for homee."""
async_add_devices(
HomeeSiren(attribute, config_entry)
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type == AttributeType.SIREN
)
class HomeeSiren(HomeeEntity, SirenEntity):
"""Representation of a homee siren device."""
_attr_name = None
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
@property
def is_on(self) -> bool:
"""Return the state of the siren."""
return self._attribute.current_value == 1.0
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the siren on."""
await self.async_set_homee_value(1)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the siren off."""
await self.async_set_homee_value(0)
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "silver",
"requirements": ["aioimmich==0.6.0"]
"requirements": ["aioimmich==0.7.0"]
}
+70 -46
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from logging import getLogger
import mimetypes
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.exceptions import ImmichError
@@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Immich media source."""
entries = hass.config_entries.async_entries(
DOMAIN, include_disabled=False, include_ignore=False
)
hass.http.register_view(ImmichMediaView(hass))
return ImmichMediaSource(hass, entries)
return ImmichMediaSource(hass)
class ImmichMediaSourceIdentifier:
@@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
def __init__(self, identifier: str) -> None:
"""Split identifier into parts."""
parts = identifier.split("/")
# coonfig_entry.unique_id/album_id/asset_it/filename
parts = identifier.split("|")
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
self.unique_id = parts[0]
self.album_id = parts[1] if len(parts) > 1 else None
self.asset_id = parts[2] if len(parts) > 2 else None
self.file_name = parts[3] if len(parts) > 2 else None
self.collection = parts[1] if len(parts) > 1 else None
self.collection_id = parts[2] if len(parts) > 2 else None
self.asset_id = parts[3] if len(parts) > 3 else None
self.file_name = parts[4] if len(parts) > 3 else None
self.mime_type = parts[5] if len(parts) > 3 else None
class ImmichMediaSource(MediaSource):
@@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
name = "Immich"
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize Immich media source."""
super().__init__(DOMAIN)
self.hass = hass
self.entries = entries
async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Immich is not configured")
return BrowseMediaSource(
domain=DOMAIN,
@@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_immich(item),
*await self._async_build_immich(item, entries),
],
)
async def _async_build_immich(
self, item: MediaSourceItem
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
if not item.identifier:
LOGGER.debug("Render all Immich instances")
return [
BrowseMediaSource(
domain=DOMAIN,
@@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
can_play=False,
can_expand=True,
)
for entry in self.entries
for entry in entries
]
identifier = ImmichMediaSourceIdentifier(item.identifier)
entry: ImmichConfigEntry | None = (
@@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
assert entry
immich_api = entry.runtime_data.api
if identifier.album_id is None:
# Get Albums
if identifier.collection is None:
LOGGER.debug("Render all collections for %s", entry.title)
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|albums",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="albums",
can_play=False,
can_expand=True,
)
]
if identifier.collection_id is None:
LOGGER.debug("Render all albums for %s", entry.title)
try:
albums = await immich_api.albums.async_get_all_albums()
except ImmichError:
@@ -118,21 +130,25 @@ class ImmichMediaSource(MediaSource):
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{item.identifier}/{album.album_id}",
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title=album.name,
can_play=False,
can_expand=True,
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg",
)
for album in albums
]
# Request items of album
LOGGER.debug(
"Render all assets of album %s for %s",
identifier.collection_id,
entry.title,
)
try:
album_info = await immich_api.albums.async_get_album_info(
identifier.album_id
identifier.collection_id
)
except ImmichError:
return []
@@ -141,17 +157,18 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}|"
f"{asset.asset_id}|"
f"{asset.file_name}|"
f"{asset.mime_type}"
),
media_class=MediaClass.IMAGE,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=False,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}",
)
for asset in album_info.assets
if asset.mime_type.startswith("image/")
@@ -161,17 +178,18 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}|"
f"{asset.asset_id}|"
f"{asset.file_name}|"
f"{asset.mime_type}"
),
media_class=MediaClass.VIDEO,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=True,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg",
)
for asset in album_info.assets
if asset.mime_type.startswith("video/")
@@ -181,17 +199,23 @@ class ImmichMediaSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
identifier = ImmichMediaSourceIdentifier(item.identifier)
if identifier.file_name is None:
raise Unresolvable("No file name")
mime_type, _ = mimetypes.guess_type(identifier.file_name)
if not isinstance(mime_type, str):
raise Unresolvable("No file extension")
try:
identifier = ImmichMediaSourceIdentifier(item.identifier)
except IndexError as err:
raise Unresolvable(
f"Could not parse identifier: {item.identifier}"
) from err
if identifier.mime_type is None:
raise Unresolvable(
f"Could not resolve identifier that has no mime-type: {item.identifier}"
)
return PlayMedia(
(
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
),
mime_type,
identifier.mime_type,
)
@@ -212,10 +236,10 @@ class ImmichMediaView(HomeAssistantView):
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
raise HTTPNotFound
asset_id, file_name, size = location.split("/")
mime_type, _ = mimetypes.guess_type(file_name)
if not isinstance(mime_type, str):
raise HTTPNotFound
try:
asset_id, size, mime_type_base, mime_type_format = location.split("/")
except ValueError as err:
raise HTTPNotFound from err
entry: ImmichConfigEntry | None = (
self.hass.config_entries.async_entry_for_domain_unique_id(
@@ -226,7 +250,7 @@ class ImmichMediaView(HomeAssistantView):
immich_api = entry.runtime_data.api
# stream response for videos
if mime_type.startswith("video/"):
if mime_type_base == "video":
try:
resp = await immich_api.assets.async_play_video_stream(asset_id)
except ImmichError as exc:
@@ -243,4 +267,4 @@ class ImmichMediaView(HomeAssistantView):
image = await immich_api.assets.async_view_asset(asset_id, size)
except ImmichError as exc:
raise HTTPNotFound from exc
return Response(body=image, content_type=mime_type)
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")
+16 -1
View File
@@ -11,8 +11,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard"
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Keyboard",
},
)
keyboard = PyKeyboard()
keyboard.special_key_assignment()
@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.6"]
"requirements": ["pylamarzocco==2.0.7"]
}
@@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_on",
translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off",
translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
@@ -3,6 +3,7 @@
"name": "LG ThinQ",
"codeowners": ["@LG-ThinQ-Integration"],
"config_flow": true,
"dhcp": [{ "macaddress": "34E6E6*" }],
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],
+16 -1
View File
@@ -7,8 +7,9 @@ import time
import lirc
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIRC capability."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "LIRC",
},
)
# blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully
# on exit.
+1 -22
View File
@@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MinPINCodeLength",
translation_key="min_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MaxPINCodeLength",
translation_key="max_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="TargetPositionLiftPercent100ths",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
translation_key="window_covering_target_position",
measurement_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE,
@@ -390,12 +390,6 @@
"evse_user_max_charge_current": {
"name": "User max charge current"
},
"min_pin_code_length": {
"name": "Min PIN code length"
},
"max_pin_code_length": {
"name": "Max PIN code length"
},
"window_covering_target_position": {
"name": "Target opening position"
}
@@ -63,16 +63,6 @@
}
}
},
"issues": {
"deprecated_yaml_import_issue_invalid_auth": {
"title": "The MELCloud YAML configuration import failed",
"description": "Configuring MELCloud using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
},
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The MELCloud YAML configuration import failed",
"description": "Configuring MELCloud using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to MELCloud works and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
}
},
"entity": {
"sensor": {
"room_temperature": {
+20 -4
View File
@@ -39,6 +39,7 @@ from homeassistant.components.light import (
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
STATE_CLASS_UNITS,
SensorDeviceClass,
SensorStateClass,
)
@@ -640,6 +641,13 @@ def validate_sensor_platform_config(
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
if (
(state_class := config.get(CONF_STATE_CLASS)) is not None
and state_class in STATE_CLASS_UNITS
and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class]
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class"
return errors
@@ -676,11 +684,19 @@ class PlatformField:
@callback
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
"""Return a context based unit of measurement selector."""
if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS:
return SelectSelector(
SelectSelectorConfig(
options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]],
sort=True,
custom_value=True,
)
)
if (
user_data is None
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
or device_class not in DEVICE_CLASS_UNITS
):
device_class := user_data.get(CONF_DEVICE_CLASS)
) is None or device_class not in DEVICE_CLASS_UNITS:
return TEXT_SELECTOR
return SelectSelector(
SelectSelectorConfig(
+12
View File
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT,
STATE_CLASS_UNITS,
STATE_CLASSES_SCHEMA,
RestoreSensor,
SensorDeviceClass,
@@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
)
if (
(state_class := config.get(CONF_STATE_CLASS)) is not None
and state_class in STATE_CLASS_UNITS
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT))
not in STATE_CLASS_UNITS[state_class]
):
raise vol.Invalid(
f"The unit of measurement '{unit_of_measurement}' is not valid "
f"together with state class '{state_class}'"
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:
@@ -644,6 +644,7 @@
"invalid_template": "Invalid template",
"invalid_supported_color_modes": "Invalid supported color modes selection",
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_url": "Invalid URL",
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",
@@ -446,4 +446,5 @@ class NestFlowHandler(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
await self._async_handle_discovery_without_unique_id()
return await self.async_step_user()
@@ -58,15 +58,3 @@ class NikoHomeControlConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
"""Import a config entry."""
self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]})
error = await test_connection(import_info[CONF_HOST])
if not error:
return self.async_create_entry(
title="Niko Home Control",
data={CONF_HOST: import_info[CONF_HOST]},
)
return self.async_abort(reason=error)
@@ -5,80 +5,19 @@ from __future__ import annotations
from typing import Any
from nhc.light import NHCLight
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
brightness_supported,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NHCController, NikoHomeControlConfigEntry
from .const import DOMAIN
from .entity import NikoHomeControlEntity
# delete after 2025.7.0
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Niko Home Control light platform."""
# Start import flow
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
if (
result.get("type") == FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result['reason']}",
breaks_in_ha_version="2025.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Niko Home Control",
},
)
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Niko Home Control",
},
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -17,11 +17,5 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"title": "YAML import failed due to a connection error",
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ SUPPORT_FLAGS = (
PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
MIN_TEMPERATURE = 7
MAX_TEMPERATURE = 40
MAX_TEMPERATURE = 30
async def async_setup_entry(
+2 -2
View File
@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NukiEntryData
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN as NUKI_DOMAIN, ERROR_STATES
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN, ERROR_STATES
from .entity import NukiEntity
from .helpers import CannotConnect
@@ -29,7 +29,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Nuki lock platform."""
entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id]
entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id]
coordinator = entry_data.coordinator
entities: list[NukiDeviceEntity] = [
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
from .entity import OneWireEntity, OneWireEntityDescription
from .onewirehub import (
SIGNAL_NEW_DEVICE_CONNECTED,
@@ -37,13 +37,14 @@ class OneWireBinarySensorEntityDescription(
):
"""Class describing OneWire binary sensor entities."""
read_mode = READ_MODE_INT
DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
"12": tuple(
OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id",
translation_placeholders={"id": str(device_key)},
)
@@ -53,7 +54,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id",
translation_placeholders={"id": str(device_key)},
)
@@ -63,7 +63,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id",
translation_placeholders={"id": str(device_key)},
)
@@ -78,7 +77,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
OneWireBinarySensorEntityDescription(
key=f"hub/short.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.PROBLEM,
translation_key="hub_short_id",
@@ -162,4 +160,4 @@ class OneWireBinarySensorEntity(OneWireEntity, BinarySensorEntity):
"""Return true if sensor is on."""
if self._state is None:
return None
return bool(self._state)
return self._state == 1
@@ -51,6 +51,5 @@ MANUFACTURER_MAXIM = "Maxim Integrated"
MANUFACTURER_HOBBYBOARDS = "Hobby Boards"
MANUFACTURER_EDS = "Embedded Data Systems"
READ_MODE_BOOL = "bool"
READ_MODE_FLOAT = "float"
READ_MODE_INT = "int"
+2 -5
View File
@@ -10,9 +10,8 @@ from pyownet import protocol
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import StateType
from .const import READ_MODE_BOOL, READ_MODE_INT
from .const import READ_MODE_INT
@dataclass(frozen=True)
@@ -45,7 +44,7 @@ class OneWireEntity(Entity):
self._attr_unique_id = f"/{device_id}/{description.key}"
self._attr_device_info = device_info
self._device_file = device_file
self._state: StateType = None
self._state: int | float | None = None
self._value_raw: float | None = None
self._owproxy = owproxy
@@ -82,7 +81,5 @@ class OneWireEntity(Entity):
_LOGGER.debug("Fetching %s data recovered", self.name)
if self.entity_description.read_mode == READ_MODE_INT:
self._state = int(self._value_raw)
elif self.entity_description.read_mode == READ_MODE_BOOL:
self._state = int(self._value_raw) == 1
else:
self._state = self._value_raw
+4 -12
View File
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
from .entity import OneWireEntity, OneWireEntityDescription
from .onewirehub import (
SIGNAL_NEW_DEVICE_CONNECTED,
@@ -32,13 +32,14 @@ SCAN_INTERVAL = timedelta(seconds=30)
class OneWireSwitchEntityDescription(OneWireEntityDescription, SwitchEntityDescription):
"""Class describing OneWire switch entities."""
read_mode = READ_MODE_INT
DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
"05": (
OneWireSwitchEntityDescription(
key="PIO",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio",
),
),
@@ -47,7 +48,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"PIO.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id",
translation_placeholders={"id": str(device_key)},
)
@@ -57,7 +57,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"latch.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="latch_id",
translation_placeholders={"id": str(device_key)},
)
@@ -69,7 +68,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key="IAD",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
read_mode=READ_MODE_BOOL,
translation_key="iad",
),
),
@@ -78,7 +76,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"PIO.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id",
translation_placeholders={"id": str(device_key)},
)
@@ -88,7 +85,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"latch.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="latch_id",
translation_placeholders={"id": str(device_key)},
)
@@ -99,7 +95,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"PIO.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id",
translation_placeholders={"id": str(device_key)},
)
@@ -115,7 +110,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"hub/branch.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
translation_key="hub_branch_id",
translation_placeholders={"id": str(device_key)},
@@ -127,7 +121,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"moisture/is_leaf.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
translation_key="leaf_sensor_id",
translation_placeholders={"id": str(device_key)},
@@ -138,7 +131,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"moisture/is_moisture.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
translation_key="moisture_sensor_id",
translation_placeholders={"id": str(device_key)},
@@ -226,7 +218,7 @@ class OneWireSwitchEntity(OneWireEntity, SwitchEntity):
"""Return true if switch is on."""
if self._state is None:
return None
return bool(self._state)
return self._state == 1
def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
@@ -78,7 +78,7 @@
"message": "Error connecting to the Overseerr instance: {error}"
},
"auth_error": {
"message": "Invalid API key."
"message": "[%key:common::config_flow::error::invalid_api_key%]"
},
"not_loaded": {
"message": "{target} is not loaded."
@@ -1 +1,3 @@
"""The pandora component."""
DOMAIN = "pandora"
@@ -27,10 +27,13 @@ from homeassistant.const import (
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_UP,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -53,6 +56,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Pandora media player platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Pandora",
},
)
if not _pianobar_exists():
return
pandora = PandoraMediaPlayer("Pandora")
@@ -26,7 +26,7 @@ from .coordinator import (
PaperlessStatusCoordinator,
)
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.UPDATE]
async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool:
@@ -16,6 +16,7 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"pngx_version": entry.runtime_data.status.api.host_version,
"data": {
"statistics": asdict(entry.runtime_data.statistics.data),
"status": asdict(entry.runtime_data.status.data),
@@ -126,6 +126,11 @@
"error": "[%key:common::state::error%]"
}
}
},
"update": {
"paperless_update": {
"name": "Software"
}
}
},
"exceptions": {
@@ -0,0 +1,90 @@
"""Update platform for Paperless-ngx."""
from __future__ import annotations
from datetime import timedelta
from pypaperless.exceptions import PaperlessConnectionError
from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import LOGGER
from .coordinator import PaperlessConfigEntry, PaperlessStatusCoordinator
from .entity import PaperlessEntity
PAPERLESS_CHANGELOGS = "https://docs.paperless-ngx.com/changelog/"
PARALLEL_UPDATES = 1
SCAN_INTERVAL = timedelta(hours=24)
async def async_setup_entry(
hass: HomeAssistant,
entry: PaperlessConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Paperless-ngx update entities."""
description = UpdateEntityDescription(
key="paperless_update",
translation_key="paperless_update",
device_class=UpdateDeviceClass.FIRMWARE,
)
async_add_entities(
[
PaperlessUpdate(
coordinator=entry.runtime_data.status,
description=description,
)
],
update_before_add=True,
)
class PaperlessUpdate(PaperlessEntity[PaperlessStatusCoordinator], UpdateEntity):
"""Defines a Paperless-ngx update entity."""
release_url = PAPERLESS_CHANGELOGS
@property
def should_poll(self) -> bool:
"""Return True because we need to poll the latest version."""
return True
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._attr_available
@property
def installed_version(self) -> str | None:
"""Return the installed version."""
return self.coordinator.api.host_version
async def async_update(self) -> None:
"""Update the entity."""
remote_version = None
try:
remote_version = await self.coordinator.api.remote_version()
except PaperlessConnectionError as err:
if self._attr_available:
LOGGER.warning("Could not fetch remote version: %s", err)
self._attr_available = False
return
if remote_version.version is None or remote_version.version == "0.0.0":
if self._attr_available:
LOGGER.warning("Remote version is not available or invalid")
self._attr_available = False
return
self._attr_latest_version = remote_version.version.lstrip("v")
self._attr_available = True
@@ -7,7 +7,7 @@
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": [
"SQLAlchemy==2.0.40",
"SQLAlchemy==2.0.41",
"fnv-hash-fast==1.5.0",
"psutil-home-assistant==0.0.1"
]
@@ -233,6 +233,14 @@ async def async_setup_entry(
"privacy_mode_change", async_privacy_mode_change, 623
)
# ensure host device is setup before connected camera devices that use via_device
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, host.unique_id)},
connections={(dr.CONNECTION_NETWORK_MAC, host.api.mac_address)},
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(
@@ -462,6 +462,12 @@
"doorbell_button_sound": {
"default": "mdi:volume-high"
},
"hardwired_chime_enabled": {
"default": "mdi:bell",
"state": {
"off": "mdi:bell-off"
}
},
"hdr": {
"default": "mdi:hdr"
},
@@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.13.3"]
"requirements": ["reolink-aio==0.13.4"]
}
@@ -910,6 +910,9 @@
"auto_focus": {
"name": "Auto focus"
},
"hardwired_chime_enabled": {
"name": "Hardwired chime enabled"
},
"guard_return": {
"name": "Guard return"
},
@@ -216,6 +216,16 @@ SWITCH_ENTITIES = (
value=lambda api, ch: api.baichuan.privacy_mode(ch),
method=lambda api, ch, value: api.baichuan.set_privacy_mode(ch, value),
),
ReolinkSwitchEntityDescription(
key="hardwired_chime_enabled",
cmd_key="483",
translation_key="hardwired_chime_enabled",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
supported=lambda api, ch: api.supported(ch, "hardwired_chime"),
value=lambda api, ch: api.baichuan.hardwired_chime_enabled(ch),
method=lambda api, ch, value: api.baichuan.set_ding_dong_ctrl(ch, enable=value),
),
)
NVR_SWITCH_ENTITIES = (
+26 -1
View File
@@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView):
verify_ssl=False,
ssl_cipher=SSLCipherList.INSECURE,
)
self._vod_type: str | None = None
async def get(
self,
@@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView):
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
ch = int(channel)
if self._vod_type is not None:
vod_type = self._vod_type
try:
host = get_host(self.hass, config_entry_id)
except Unresolvable:
@@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView):
"apolication/octet-stream",
]:
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
if (
reolink_response.content_type == "video/x-flv"
and vod_type == VodRequestType.PLAYBACK.value
):
# next time use DOWNLOAD immediately
self._vod_type = VodRequestType.DOWNLOAD.value
_LOGGER.debug(
"%s, retrying using download instead of playback cmd", err_str
)
return await self.get(
request,
config_entry_id,
channel,
stream_res,
self._vod_type,
filename,
retry,
)
_LOGGER.error(err_str)
if reolink_response.content_type == "text/html":
text = await reolink_response.text()
@@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView):
reolink_response.reason,
response_headers,
)
response_headers["Content-Type"] = "video/mp4"
if "Content-Type" not in response_headers:
response_headers["Content-Type"] = reolink_response.content_type
if response_headers["Content-Type"] == "apolication/octet-stream":
response_headers["Content-Type"] = "application/octet-stream"
response = web.StreamResponse(
status=reolink_response.status,
+1 -16
View File
@@ -13,9 +13,7 @@ from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
)
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
@@ -181,18 +179,5 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
self.entity_id, variables, None
)
if value is None or self.device_class not in (
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
):
self._attr_native_value = value
self._process_manual_data(variables)
self.async_write_ha_state()
return
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._process_manual_data(variables)
self._set_value_with_possible_timestamp(value, self.device_class, variables)
self.async_write_ha_state()
+2 -14
View File
@@ -7,8 +7,7 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.components.sensor import CONF_STATE_CLASS
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
@@ -218,18 +217,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
self.entity_id, variables, None
)
if self.device_class not in {
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
}:
self._attr_native_value = value
self._process_manual_data(variables)
return
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._process_manual_data(variables)
self._set_value_with_possible_timestamp(value, self.device_class, variables)
@property
def available(self) -> bool:
+1 -1
View File
@@ -135,7 +135,7 @@
"name": "State class",
"state": {
"measurement": "Measurement",
"measurement_angle": "Measurement Angle",
"measurement_angle": "Measurement angle",
"total": "Total",
"total_increasing": "Total increasing"
}
@@ -92,13 +92,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"""Mark the first item with matching `name` as completed."""
data = hass.data[DOMAIN]
name = call.data[ATTR_NAME]
try:
item = [item for item in data.items if item["name"] == name][0]
except IndexError:
_LOGGER.error("Updating of item failed: %s cannot be found", name)
else:
await data.async_update(item["id"], {"name": name, "complete": True})
await data.async_complete(name)
except NoMatchingShoppingListItem:
_LOGGER.error("Completing of item failed: %s cannot be found", name)
async def incomplete_item_service(call: ServiceCall) -> None:
"""Mark the first item with matching `name` as incomplete."""
@@ -258,6 +255,30 @@ class ShoppingData:
)
return removed
async def async_complete(
self, name: str, context: Context | None = None
) -> list[dict[str, JsonValueType]]:
"""Mark all shopping list items with the given name as complete."""
complete_items = [
item for item in self.items if item["name"] == name and not item["complete"]
]
if len(complete_items) == 0:
raise NoMatchingShoppingListItem
for item in complete_items:
_LOGGER.debug("Completing %s", item)
item["complete"] = True
await self.hass.async_add_executor_job(self.save)
self._async_notify()
for item in complete_items:
self.hass.bus.async_fire(
EVENT_SHOPPING_LIST_UPDATED,
{"action": "complete", "item": item},
context=context,
)
return complete_items
async def async_update(
self, item_id: str | None, info: dict[str, Any], context: Context | None = None
) -> dict[str, JsonValueType]:
@@ -5,15 +5,17 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, intent
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED, NoMatchingShoppingListItem
INTENT_ADD_ITEM = "HassShoppingListAddItem"
INTENT_COMPLETE_ITEM = "HassShoppingListCompleteItem"
INTENT_LAST_ITEMS = "HassShoppingListLastItems"
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the Shopping List intents."""
intent.async_register(hass, AddItemIntent())
intent.async_register(hass, CompleteItemIntent())
intent.async_register(hass, ListTopItemsIntent())
@@ -36,6 +38,33 @@ class AddItemIntent(intent.IntentHandler):
return response
class CompleteItemIntent(intent.IntentHandler):
"""Handle CompleteItem intents."""
intent_type = INTENT_COMPLETE_ITEM
description = "Marks an item as completed on the shopping list"
slot_schema = {"item": cv.string}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"].strip()
try:
complete_items = await intent_obj.hass.data[DOMAIN].async_complete(item)
except NoMatchingShoppingListItem:
complete_items = []
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
response = intent_obj.create_response()
response.async_set_speech_slots({"completed_items": complete_items})
response.response_type = intent.IntentResponseType.ACTION_DONE
return response
class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents."""
@@ -47,7 +76,7 @@ class ListTopItemsIntent(intent.IntentHandler):
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response()
response: intent.IntentResponse = intent_obj.create_response()
if not items:
response.async_set_speech("There are no items on your shopping list")
+16 -4
View File
@@ -1,25 +1,37 @@
"""Common base for entities."""
from dataclasses import dataclass
from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity import Entity, EntityDescription
from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
@dataclass(frozen=True, kw_only=True)
class SmarlaEntityDescription(EntityDescription):
"""Class describing Swing2Sleep Smarla entities."""
service: str
property: str
class SmarlaBaseEntity(Entity):
"""Common Base Entity class for defining Smarla device."""
entity_description: SmarlaEntityDescription
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, federwiege: Federwiege, prop: Property) -> None:
def __init__(self, federwiege: Federwiege, desc: SmarlaEntityDescription) -> None:
"""Initialise the entity."""
self._property = prop
self.entity_description = desc
self._property = federwiege.get_property(desc.service, desc.property)
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, federwiege.serial_number)},
name=DEVICE_MODEL_NAME,
+2 -17
View File
@@ -3,7 +3,6 @@
from dataclasses import dataclass
from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
@@ -11,16 +10,13 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FederwiegeConfigEntry
from .entity import SmarlaBaseEntity
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
@dataclass(frozen=True, kw_only=True)
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
class SmarlaSwitchEntityDescription(SmarlaEntityDescription, SwitchEntityDescription):
"""Class describing Swing2Sleep Smarla switch entity."""
service: str
property: str
SWITCHES: list[SmarlaSwitchEntityDescription] = [
SmarlaSwitchEntityDescription(
@@ -55,17 +51,6 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
_property: Property[bool]
def __init__(
self,
federwiege: Federwiege,
desc: SmarlaSwitchEntityDescription,
) -> None:
"""Initialize a Smarla switch."""
prop = federwiege.get_property(desc.service, desc.property)
super().__init__(federwiege, prop)
self.entity_description = desc
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
@property
def is_on(self) -> bool:
"""Return the entity value to represent the entity state."""
+23 -1
View File
@@ -6,9 +6,14 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
},
extra=vol.ALLOW_EXTRA,
)
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -52,6 +58,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure Gammu state machine."""
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
DEPRECATED_ISSUE_ID,
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_config_flow_integration",
translation_placeholders={
"integration_title": "SMS notifications via GSM-modem",
},
)
device = entry.data[CONF_DEVICE]
connection_mode = "at"
@@ -101,4 +120,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
await gateway.terminate_async()
if not hass.config_entries.async_loaded_entries(DOMAIN):
async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID)
return unload_ok
+1 -2
View File
@@ -217,8 +217,7 @@ class SnmpSensor(ManualTriggerSensorEntity):
self.entity_id, variables, STATE_UNKNOWN
)
self._attr_native_value = value
self._process_manual_data(variables)
self._set_value_with_possible_timestamp(value, self.device_class, variables)
class SnmpData:
+1 -1
View File
@@ -6,5 +6,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling",
"requirements": ["SQLAlchemy==2.0.40", "sqlparse==0.5.0"]
"requirements": ["SQLAlchemy==2.0.41", "sqlparse==0.5.0"]
}
+4 -2
View File
@@ -401,10 +401,12 @@ class SQLSensor(ManualTriggerSensorEntity):
if data is not None and self._template is not None:
variables = self._template_variables_with_value(data)
if self._render_availability_template(variables):
self._attr_native_value = self._template.async_render_as_value_template(
_value = self._template.async_render_as_value_template(
self.entity_id, variables, None
)
self._process_manual_data(variables)
self._set_value_with_possible_timestamp(
_value, self.device_class, variables
)
else:
self._attr_native_value = data
+4 -4
View File
@@ -26,14 +26,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN as SWITCH_DOMAIN
from .const import DOMAIN
DEFAULT_NAME = "Light Switch"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
}
)
@@ -76,7 +76,7 @@ class LightSwitch(LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Forward the turn_on command to the switch in this light switch."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
@@ -86,7 +86,7 @@ class LightSwitch(LightEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Forward the turn_off command to the switch in this light switch."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
@@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, ToggleEntity
from homeassistant.helpers.event import async_track_state_change_event
from .const import DOMAIN as SWITCH_AS_X_DOMAIN
from .const import DOMAIN
class BaseEntity(Entity):
@@ -61,7 +61,7 @@ class BaseEntity(Entity):
self._switch_entity_id = switch_entity_id
self._is_new_entity = (
registry.async_get_entity_id(domain, SWITCH_AS_X_DOMAIN, unique_id) is None
registry.async_get_entity_id(domain, DOMAIN, unique_id) is None
)
@callback
@@ -102,7 +102,7 @@ class BaseEntity(Entity):
if registry.async_get(self.entity_id) is not None:
registry.async_update_entity_options(
self.entity_id,
SWITCH_AS_X_DOMAIN,
DOMAIN,
self.async_generate_entity_options(),
)
+1 -1
View File
@@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
SCAN_INTERVAL = timedelta(minutes=5)
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5)
class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["aiotedee"],
"quality_scale": "platinum",
"requirements": ["aiotedee==0.2.20"]
"requirements": ["aiotedee==0.2.23"]
}
@@ -1 +1,4 @@
"""The tensorflow component."""
DOMAIN = "tensorflow"
CONF_GRAPH = "graph"
@@ -26,15 +26,21 @@ from homeassistant.const import (
CONF_SOURCE,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.pil import draw_box
from . import CONF_GRAPH, DOMAIN
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
DOMAIN = "tensorflow"
_LOGGER = logging.getLogger(__name__)
ATTR_MATCHES = "matches"
@@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom"
CONF_CATEGORIES = "categories"
CONF_CATEGORY = "category"
CONF_FILE_OUT = "file_out"
CONF_GRAPH = "graph"
CONF_LABELS = "labels"
CONF_LABEL_OFFSET = "label_offset"
CONF_LEFT = "left"
@@ -110,6 +115,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the TensorFlow image processing platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Tensorflow",
},
)
model_config = config[CONF_MODEL]
model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow")
labels = model_config.get(CONF_LABELS) or hass.config.path(
@@ -20,6 +20,10 @@ STATES = {
"Stopped": MediaPlayerState.IDLE,
}
# Tesla uses 31 steps, in 0.333 increments up to 10.333
VOLUME_STEP = 1 / 31
VOLUME_FACTOR = 31 / 3 # 10.333
PARALLEL_UPDATES = 0
@@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
"""Vehicle Location Media Class."""
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_volume_step = VOLUME_STEP
def __init__(
self,
@@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
@property
def volume_level(self) -> float:
"""Volume level of the media player (0..1)."""
return self.get("vehicle_state_media_info_audio_volume", 0) / self.get(
"vehicle_state_media_info_audio_volume_max", 10.333333
)
return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR
@property
def media_duration(self) -> int | None:
@@ -34,6 +34,9 @@ async def get_base_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schem
"""Get base options schema."""
return vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(multiple=False, read_only=True),
),
vol.Optional(CONF_ATTRIBUTE): selector.AttributeSelector(
selector.AttributeSelectorConfig(
entity_id=handler.options[CONF_ENTITY_ID]
@@ -18,6 +18,7 @@
},
"settings": {
"data": {
"entity_id": "[%key:component::trend::config::step::user::data::entity_id%]",
"attribute": "Attribute of entity that this sensor tracks",
"invert": "Invert the result"
}
@@ -28,6 +29,7 @@
"step": {
"init": {
"data": {
"entity_id": "[%key:component::trend::config::step::user::data::entity_id%]",
"attribute": "[%key:component::trend::config::step::settings::data::attribute%]",
"invert": "[%key:component::trend::config::step::settings::data::invert%]",
"max_samples": "Maximum number of stored samples",
+2 -2
View File
@@ -16,7 +16,7 @@ from homeassistant.helpers.device_registry import (
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN, PLATFORMS
from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN, PLATFORMS
from .config import UnifiConfig
from .entity_helper import UnifiEntityHelper
from .entity_loader import UnifiEntityLoader
@@ -104,7 +104,7 @@ class UnifiHub:
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(UNIFI_DOMAIN, self.config.entry.unique_id)},
identifiers={(DOMAIN, self.config.entry.unique_id)},
manufacturer=ATTR_MANUFACTURER,
model="UniFi Network Application",
name="UniFi Network",
+3 -5
View File
@@ -52,7 +52,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import UnifiConfigEntry
from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
from .const import ATTR_MANUFACTURER, DOMAIN
from .entity import (
HandlerT,
SubscriptionT,
@@ -367,14 +367,12 @@ def async_update_unique_id(hass: HomeAssistant, config_entry: UnifiConfigEntry)
def update_unique_id(obj_id: str, type_name: str) -> None:
"""Rework unique ID."""
new_unique_id = f"{type_name}-{obj_id}"
if ent_reg.async_get_entity_id(SWITCH_DOMAIN, UNIFI_DOMAIN, new_unique_id):
if ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, new_unique_id):
return
prefix, _, suffix = obj_id.partition("_")
unique_id = f"{prefix}-{type_name}-{suffix}"
if entity_id := ent_reg.async_get_entity_id(
SWITCH_DOMAIN, UNIFI_DOMAIN, unique_id
):
if entity_id := ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id):
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
for obj_id in hub.api.outlets:
@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -300,7 +300,9 @@ async def handle_call_service(
translation_placeholders=err.translation_placeholders,
)
except HomeAssistantError as err:
connection.logger.exception("Unexpected exception")
connection.logger.error(
"Error during service call to %s.%s: %s", msg["domain"], msg["service"], err
)
connection.send_error(
msg["id"],
const.ERR_HOME_ASSISTANT_ERROR,
@@ -94,21 +94,59 @@ def _get_obj_holidays(
language=language,
categories=set_categories,
)
supported_languages = obj_holidays.supported_languages
default_language = obj_holidays.default_language
if default_language and not language:
# If no language is set, use the default language
LOGGER.debug("Changing language from None to %s", default_language)
return country_holidays( # Return default if no language
country,
subdiv=province,
years=year,
language=default_language,
categories=set_categories,
)
if (
(supported_languages := obj_holidays.supported_languages)
default_language
and language
and language not in supported_languages
and language.startswith("en")
):
# If language does not match supported languages, use the first English variant
if default_language.startswith("en"):
LOGGER.debug("Changing language from %s to %s", language, default_language)
return country_holidays( # Return default English if default language
country,
subdiv=province,
years=year,
language=default_language,
categories=set_categories,
)
for lang in supported_languages:
if lang.startswith("en"):
obj_holidays = country_holidays(
LOGGER.debug("Changing language from %s to %s", language, lang)
return country_holidays(
country,
subdiv=province,
years=year,
language=lang,
categories=set_categories,
)
LOGGER.debug("Changing language from %s to %s", language, lang)
if default_language and language and language not in supported_languages:
# If language does not match supported languages, use the default language
LOGGER.debug("Changing language from %s to %s", language, default_language)
return country_holidays( # Return default English if default language
country,
subdiv=province,
years=year,
language=default_language,
categories=set_categories,
)
return obj_holidays
@@ -67,8 +67,7 @@ def add_province_and_language_to_schema(
_country = country_holidays(country=country)
if country_default_language := (_country.default_language):
selectable_languages = _country.supported_languages
new_selectable_languages = list(selectable_languages)
new_selectable_languages = list(_country.supported_languages)
language_schema = {
vol.Optional(
CONF_LANGUAGE, default=country_default_language
@@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None:
years=year,
language=language,
)
if (
(supported_languages := obj_holidays.supported_languages)
and language
and language.startswith("en")
):
for lang in supported_languages:
if lang.startswith("en"):
obj_holidays = country_holidays(
country,
subdiv=province,
years=year,
language=lang,
)
else:
obj_holidays = HolidayBase(years=year)

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