Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
13105bd
Migrate cover platform to entity descriptions in Overkiz (#141330)
iMicknl Apr 23, 2026
49022b6
Add MQTT time platform (#168898)
jbouwh Apr 23, 2026
fbf30e6
Migrate to entity services in amcrest (#168974)
arturpragacz Apr 23, 2026
94ca503
Media browser: apply sentence-style capitalization (#168971)
c0ffeeca7 Apr 23, 2026
19fd6e2
Fix b&o race conditions for Python 3.14.3 (#168885)
justanotherariel Apr 23, 2026
8448ace
Migrate `shopping_list` to use `entry.runtime_data` (#168911)
mib1185 Apr 23, 2026
4612a72
Add reconfiguration flow to Fumis integration (#168759)
frenck Apr 23, 2026
3f2bc45
Migrate to entity services in monoprice (#168972)
arturpragacz Apr 23, 2026
0122b28
Bump python-bsblan to 5.2.0 (#168892)
liudger Apr 23, 2026
1cd34e8
Victron GX stale devices (#168706)
tomer-w Apr 23, 2026
5394c76
Victron GX: Add strict typing (#168907)
tomer-w Apr 23, 2026
7bf3e75
Fix invalid notification/event handling in Tuya tests (#168854)
epenet Apr 23, 2026
86ffb9e
Victron GX: Add exception translations (#168762)
tomer-w Apr 23, 2026
b4c8452
Add open (unlatch) support to Homee locks (#168532)
kostavelikov Apr 23, 2026
e5cd1e2
Tessie: log warning instead of raising UpdateFailed for missing energ…
fender4645 Apr 23, 2026
0e817c5
Bump HueBLE to 2.2.2 (#167677)
flip-dots Apr 23, 2026
0bb678c
Bump uiprotect to 10.3.0 (#168992)
RaHehl Apr 23, 2026
20a88eb
Add entity availability to Satel Integra (#168476)
Tommatheussen Apr 23, 2026
e1a73fb
Add select platform tests for nobo_hub (#168738)
oyvindwe Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ homeassistant.components.vallox.*
homeassistant.components.valve.*
homeassistant.components.velbus.*
homeassistant.components.velux.*
homeassistant.components.victron_gx.*
homeassistant.components.vivotek.*
homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.*
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/ai_task/media_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
hass.data[DATA_MEDIA_SOURCE] = source = local_source.LocalSource(
hass,
DOMAIN,
"AI Generated Images",
"AI generated images",
{IMAGE_DIR: str(media_dir)},
f"/{DOMAIN}",
)
Expand Down
3 changes: 1 addition & 2 deletions homeassistant/components/amcrest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
from .camera import STREAM_SOURCE_LIST
from .const import (
CAMERAS,
COMM_RETRIES,
COMM_TIMEOUT,
DATA_AMCREST,
Expand Down Expand Up @@ -359,7 +358,7 @@ def _start_event_monitor(

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Amcrest IP Camera component."""
hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []})
hass.data.setdefault(DATA_AMCREST, {DEVICES: {}})

for device in config[DOMAIN]:
name: str = device[CONF_NAME]
Expand Down
84 changes: 10 additions & 74 deletions homeassistant/components/amcrest/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
from aiohttp import web
from amcrest import AmcrestError
from haffmpeg.camera import CameraMjpeg
import voluptuous as vol

from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream,
async_aiohttp_proxy_web,
Expand All @@ -29,11 +27,13 @@
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from .const import (
ATTR_COLOR_BW,
CAMERA_WEB_SESSION_TIMEOUT,
CAMERAS,
CBW,
COMM_TIMEOUT,
DATA_AMCREST,
DEVICES,
MOV,
RESOLUTION_TO_STREAM,
SERVICE_UPDATE,
SNAPSHOT_TIMEOUT,
Expand All @@ -49,65 +49,11 @@

STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"]

_ATTR_PTZ_TT = "travel_time"
_ATTR_PTZ_MOV = "movement"
_MOV = [
"zoom_out",
"zoom_in",
"right",
"left",
"up",
"down",
"right_down",
"right_up",
"left_down",
"left_up",
]
_ZOOM_ACTIONS = ["ZoomWide", "ZoomTele"]
_MOVE_1_ACTIONS = ["Right", "Left", "Up", "Down"]
_MOVE_2_ACTIONS = ["RightDown", "RightUp", "LeftDown", "LeftUp"]
_ACTION = _ZOOM_ACTIONS + _MOVE_1_ACTIONS + _MOVE_2_ACTIONS

_DEFAULT_TT = 0.2

_ATTR_PRESET = "preset"
_ATTR_COLOR_BW = "color_bw"

_CBW_COLOR = "color"
_CBW_AUTO = "auto"
_CBW_BW = "bw"
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]

_SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids})
_SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend(
{vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))}
)
_SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)})
_SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend(
{
vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV),
vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float,
}
)

CAMERA_SERVICES = {
"enable_recording": (_SRV_SCHEMA, "async_enable_recording", ()),
"disable_recording": (_SRV_SCHEMA, "async_disable_recording", ()),
"enable_audio": (_SRV_SCHEMA, "async_enable_audio", ()),
"disable_audio": (_SRV_SCHEMA, "async_disable_audio", ()),
"enable_motion_recording": (_SRV_SCHEMA, "async_enable_motion_recording", ()),
"disable_motion_recording": (_SRV_SCHEMA, "async_disable_motion_recording", ()),
"goto_preset": (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)),
"set_color_bw": (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)),
"start_tour": (_SRV_SCHEMA, "async_start_tour", ()),
"stop_tour": (_SRV_SCHEMA, "async_stop_tour", ()),
"ptz_control": (
_SRV_PTZ_SCHEMA,
"async_ptz_control",
(_ATTR_PTZ_MOV, _ATTR_PTZ_TT),
),
}

_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}


Expand Down Expand Up @@ -275,7 +221,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
self._motion_recording_enabled
)
if self._color_bw is not None:
attr[_ATTR_COLOR_BW] = self._color_bw
attr[ATTR_COLOR_BW] = self._color_bw
return attr

@property
Expand Down Expand Up @@ -322,27 +268,17 @@ def async_on_demand_update(self) -> None:
self.async_schedule_update_ha_state(True)

async def async_added_to_hass(self) -> None:
"""Subscribe to signals and add camera to list."""
self._unsub_dispatcher.extend(
async_dispatcher_connect(
self.hass,
service_signal(service, self.entity_id),
getattr(self, callback_name),
)
for service, (_, callback_name, _) in CAMERA_SERVICES.items()
)
"""Subscribe to signals."""
self._unsub_dispatcher.append(
async_dispatcher_connect(
self.hass,
service_signal(SERVICE_UPDATE, self.name),
self.async_on_demand_update,
)
)
self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id)

async def async_will_remove_from_hass(self) -> None:
"""Remove camera from list and disconnect from signals."""
self.hass.data[DATA_AMCREST][CAMERAS].remove(self.entity_id)
"""Disconnect from signals."""
for unsub_dispatcher in self._unsub_dispatcher:
unsub_dispatcher()

Expand Down Expand Up @@ -456,7 +392,7 @@ async def async_stop_tour(self) -> None:

async def async_ptz_control(self, movement: str, travel_time: float) -> None:
"""Move or zoom camera in specified direction."""
code = _ACTION[_MOV.index(movement)]
code = _ACTION[MOV.index(movement)]

kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0}
if code in _MOVE_1_ACTIONS:
Expand Down Expand Up @@ -613,10 +549,10 @@ async def _async_goto_preset(self, preset: int) -> None:
)

async def _async_get_color_mode(self) -> str:
return _CBW[await self._api.async_day_night_color]
return CBW[await self._api.async_day_night_color]

async def _async_set_color_mode(self, cbw: str) -> None:
await self._api.async_set_day_night_color(_CBW.index(cbw), channel=0)
await self._api.async_set_day_night_color(CBW.index(cbw), channel=0)

async def _async_set_color_bw(self, cbw: str) -> None:
"""Set camera color mode."""
Expand Down
16 changes: 15 additions & 1 deletion homeassistant/components/amcrest/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

DOMAIN = "amcrest"
DATA_AMCREST = DOMAIN
CAMERAS = "cameras"
DEVICES = "devices"

BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
Expand All @@ -17,3 +16,18 @@

RESOLUTION_LIST = {"high": 0, "low": 1}
RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"}

ATTR_COLOR_BW = "color_bw"
CBW = ["color", "auto", "bw"]
MOV = [
"zoom_out",
"zoom_in",
"right",
"left",
"up",
"down",
"right_down",
"right_up",
"left_down",
"left_up",
]
109 changes: 57 additions & 52 deletions homeassistant/components/amcrest/services.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,67 @@
"""Support for Amcrest IP cameras."""
"""Services for Amcrest IP cameras."""

from __future__ import annotations

from homeassistant.auth.models import User
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import async_extract_entity_ids
import voluptuous as vol

from .camera import CAMERA_SERVICES
from .const import CAMERAS, DATA_AMCREST, DOMAIN
from .helpers import service_signal
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service

from .const import ATTR_COLOR_BW, CBW, DOMAIN, MOV

_ATTR_PRESET = "preset"
_ATTR_PTZ_MOV = "movement"
_ATTR_PTZ_TT = "travel_time"
_DEFAULT_TT = 0.2


@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Amcrest IP Camera services."""
for service_name, func in (
("enable_recording", "async_enable_recording"),
("disable_recording", "async_disable_recording"),
("enable_audio", "async_enable_audio"),
("disable_audio", "async_disable_audio"),
("enable_motion_recording", "async_enable_motion_recording"),
("disable_motion_recording", "async_disable_motion_recording"),
("start_tour", "async_start_tour"),
("stop_tour", "async_stop_tour"),
):
service.async_register_platform_entity_service(
hass,
DOMAIN,
service_name,
entity_domain=CAMERA_DOMAIN,
schema=None,
func=func,
)

def have_permission(user: User | None, entity_id: str) -> bool:
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)

async def async_extract_from_service(call: ServiceCall) -> list[str]:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None

if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
if have_permission(user, entity_id)
]

if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []

call_ids = await async_extract_entity_ids(call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids

async def async_service_handler(call: ServiceCall) -> None:
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)

for service, params in CAMERA_SERVICES.items():
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
service.async_register_platform_entity_service(
hass,
DOMAIN,
"goto_preset",
entity_domain=CAMERA_DOMAIN,
schema={vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))},
func="async_goto_preset",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"set_color_bw",
entity_domain=CAMERA_DOMAIN,
schema={vol.Required(ATTR_COLOR_BW): vol.In(CBW)},
func="async_set_color_bw",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"ptz_control",
entity_domain=CAMERA_DOMAIN,
schema={
vol.Required(_ATTR_PTZ_MOV): vol.In(MOV),
vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float,
},
func="async_ptz_control",
)
33 changes: 23 additions & 10 deletions homeassistant/components/bang_olufsen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.ssl import get_default_context

from .const import DOMAIN
from .const import DOMAIN, MANUFACTURER, BeoModel
from .services import async_setup_services
from .util import get_remotes
from .websocket import BeoWebsocket


Expand Down Expand Up @@ -58,15 +59,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
# Remove casts to str
assert entry.unique_id

# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.unique_id)},
name=entry.title,
model=entry.data[CONF_MODEL],
)

client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context())

# Check API and WebSocket connection
Expand All @@ -83,6 +75,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
await client.close_api_client()
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error

# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.unique_id)},
model=entry.data[CONF_MODEL],
)

# Create devices for paired Beoremote One remotes
for remote in await get_remotes(client):
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, f"{remote.serial_number}_{entry.unique_id}")},
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{entry.unique_id}",
model=BeoModel.BEOREMOTE_ONE,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,
via_device=(DOMAIN, entry.unique_id),
)

websocket = BeoWebsocket(hass, entry, client)

# Add the websocket and API client
Expand Down
Loading
Loading