Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

Commit 6575dc3

Browse filesBrowse the repository at this point in the historyBrowse files
authored
Merge pull request #9 from krahabb/dev
Improve compatibility up to HA core 2025.9.0
2 parents caf1cd8 + 011dbd5 commit 6575dc3
Copy full SHA for 6575dc3

11 files changed

+93-82Lines changed: 93 additions & 82 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.devcontainer/devcontainer.json‎

Copy file name to clipboardExpand all lines: .devcontainer/devcontainer.json
+5-3Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
2-
"name": "ludeeus/integration_blueprint/motion_frontend",
3-
"image": "mcr.microsoft.com/devcontainers/python:3.12",
2+
"name": "ludeeus/integration_blueprint/meross_lan",
3+
"image": "mcr.microsoft.com/devcontainers/python:3.13-bookworm",
44
"runArgs": [ "--network=host" ],
55
"postCreateCommand": "scripts/setup",
66
"customizations": {
77
"vscode": {
88
"extensions": [
99
"ms-python.python",
10-
"ms-python.vscode-pylance"
10+
"ms-python.vscode-pylance",
11+
"ms-python.black-formatter",
12+
"ms-python.isort"
1113
],
1214
"settings": {
1315
"files.eol": "\n",
Collapse file

‎.github/workflows/pull.yml‎

Copy file name to clipboardExpand all lines: .github/workflows/pull.yml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
pull_request:
55

66
env:
7-
DEFAULT_PYTHON: "3.12"
7+
DEFAULT_PYTHON: "3.13"
88

99
jobs:
1010
validate:
Collapse file

‎.github/workflows/push.yml‎

Copy file name to clipboardExpand all lines: .github/workflows/push.yml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Push actions
33
on: push
44

55
env:
6-
DEFAULT_PYTHON: "3.12"
6+
DEFAULT_PYTHON: "3.13"
77

88
jobs:
99
validate:
Collapse file

‎.vscode/settings.json‎

Copy file name to clipboardExpand all lines: .vscode/settings.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
},
55
"python.analysis.typeCheckingMode": "basic",
66
"python.analysis.diagnosticSeverityOverrides": {
7+
"reportPrivateImportUsage": "none",
78
"reportShadowedImports": "none"
89
},
910
"python.analysis.extraPaths": [
Collapse file

‎custom_components/motion_frontend/_media_source.py‎

Copy file name to clipboardExpand all lines: custom_components/motion_frontend/_media_source.py
+32-42Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
"""
2-
Local Media Source Implementation.
2+
Local Media Source Implementation.
33
4-
This code actually works to browse motion recordings on a local path
5-
allowing to browse and expose the contents of the filesystem in motion 'target_dir'
6-
into the HA media browser UI
4+
This code actually works to browse motion recordings on a local path
5+
allowing to browse and expose the contents of the filesystem in motion 'target_dir'
6+
into the HA media browser UI
77
8-
sadly, the HA media player seems unable to play the content since the url I'm publishing
9-
in 'async_resolve_media' gets processed by the default 'media_source' implementation
10-
which only looks through configured paths in 'hass.config.media_dirs'
8+
sadly, the HA media player seems unable to play the content since the url I'm publishing
9+
in 'async_resolve_media' gets processed by the default 'media_source' implementation
10+
which only looks through configured paths in 'hass.config.media_dirs'
1111
12-
Either I don't get the full picture or publishing local paths beside the official '/media'
13-
url is not allowed (which looks the most logical assumption here)
12+
Either I don't get the full picture or publishing local paths beside the official '/media'
13+
url is not allowed (which looks the most logical assumption here)
1414
15-
In the end I've reverted to a kind of a trick: when configuring the motion config entry
16-
I'll inject the 'target_dir' into the HA configured and allowed media_dirs
15+
In the end I've reverted to a kind of a trick: when configuring the motion config entry
16+
I'll inject the 'target_dir' into the HA configured and allowed media_dirs
1717
1818
1919
"""
20+
2021
from __future__ import annotations
2122

2223
import mimetypes
2324
from pathlib import Path
25+
from typing import TYPE_CHECKING, override
2426

25-
from homeassistant.components.media_player.const import (
26-
MEDIA_CLASS_DIRECTORY,
27-
MEDIA_CLASS_VIDEO,
28-
MEDIA_TYPE_VIDEO,
29-
)
27+
from homeassistant.components.media_player.const import MediaClass
3028
from homeassistant.components.media_player.errors import BrowseError
3129
from homeassistant.components.media_source.const import (
3230
MEDIA_CLASS_MAP,
@@ -44,10 +42,8 @@
4442

4543
from .const import DOMAIN
4644

47-
#from aiohttp import web
48-
#from homeassistant.components.http import HomeAssistantView
49-
50-
45+
# from aiohttp import web
46+
# from homeassistant.components.http import HomeAssistantView
5147

5248

5349
async def async_get_media_source(hass: HomeAssistant):
@@ -60,20 +56,20 @@ class ItemInfo:
6056
def __init__(self, entry_id: str, target_dir: str, relativepath: str):
6157
self.entry_id: str = entry_id
6258
self.target_dir: str = target_dir
63-
self.path: Path = Path(target_dir, relativepath) if relativepath else Path(target_dir)
59+
self.path: Path = (
60+
Path(target_dir, relativepath) if relativepath else Path(target_dir)
61+
)
6462

6563

6664
class MotionRecordSource(MediaSource):
6765
"""Provide access to motion server recordings"""
6866

6967
name: str = "Motion Server"
7068

71-
7269
def __init__(self, hass: HomeAssistant):
7370
super().__init__(DOMAIN)
7471
self.hass = hass
7572

76-
7773
@callback
7874
def async_parse_identifier(self, item: MediaSourceItem) -> ItemInfo:
7975
"""Parse identifier."""
@@ -88,7 +84,9 @@ def async_parse_identifier(self, item: MediaSourceItem) -> ItemInfo:
8884
if api is None:
8985
raise Unresolvable(f"Missing {DOMAIN} configuration entry.")
9086

91-
iteminfo = ItemInfo(entry_id, api.client.target_dir, split[1] if len(split) > 1 else None)
87+
iteminfo = ItemInfo(
88+
entry_id, api.client.target_dir, split[1] if len(split) > 1 else ""
89+
)
9290

9391
try:
9492
raise_if_invalid_path(str(iteminfo.path))
@@ -97,17 +95,15 @@ def async_parse_identifier(self, item: MediaSourceItem) -> ItemInfo:
9795

9896
return iteminfo
9997

100-
101-
async def async_resolve_media(self, item: MediaSourceItem) -> str:
98+
@override
99+
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
102100
"""Resolve media to a url."""
103101
iteminfo = self.async_parse_identifier(item)
104102
mime_type, _ = mimetypes.guess_type(str(iteminfo.path))
105-
return PlayMedia(f"/media/{item.identifier}", mime_type)
106-
103+
return PlayMedia(f"/media/{item.identifier}", mime_type or "")
107104

108-
async def async_browse_media(
109-
self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES
110-
) -> BrowseMediaSource:
105+
@override
106+
async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
111107

112108
# root node for motion_frontend media
113109
# add a child for each configured server
@@ -138,15 +134,13 @@ async def async_browse_media(
138134

139135
return base
140136

141-
142137
try:
143138
iteminfo: ItemInfo = self.async_parse_identifier(item)
144139
except Unresolvable as err:
145140
raise BrowseError(str(err)) from err
146141

147142
return await self.hass.async_add_executor_job(self._browse_media, iteminfo)
148143

149-
150144
def _browse_media(self, iteminfo: ItemInfo):
151145
# iteminfo = (entry_id, target_dir, path)
152146

@@ -155,35 +149,33 @@ def _browse_media(self, iteminfo: ItemInfo):
155149

156150
return self._build_item_response(iteminfo, iteminfo.path)
157151

158-
159152
def _build_item_response(self, iteminfo: ItemInfo, path: Path, is_child=False):
160153
mime_type, _ = mimetypes.guess_type(str(path))
154+
mime_type = mime_type or ""
161155
is_file = path.is_file()
162156
is_dir = path.is_dir()
163157

164158
# Make sure it's a file or directory
165159
if not is_file and not is_dir:
166-
return None
160+
raise BrowseError("Invalid path")
167161

168162
# Check that it's a media file
169163
if is_file and (
170164
not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES
171165
):
172-
return None
166+
raise BrowseError("Unsupported media type")
173167

174168
title = path.name
175169
if is_dir:
176170
title += "/"
177171

178-
media_class = MEDIA_CLASS_MAP.get(
179-
mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY
180-
)
172+
media_class = MEDIA_CLASS_MAP.get(mime_type and mime_type.split("/")[0], MediaClass.DIRECTORY)
181173

182174
media = BrowseMediaSource(
183175
domain=DOMAIN,
184176
identifier=f"{iteminfo.entry_id}/{path.relative_to(iteminfo.target_dir)}",
185177
media_class=media_class,
186-
media_content_type=mime_type or "",
178+
media_content_type=mime_type,
187179
title=title,
188180
can_play=is_file,
189181
can_expand=is_dir,
@@ -203,5 +195,3 @@ def _build_item_response(self, iteminfo: ItemInfo, path: Path, is_child=False):
203195
media.children.sort(key=lambda child: (child.can_play, child.title))
204196

205197
return media
206-
207-
Collapse file

‎custom_components/motion_frontend/alarm_control_panel.py‎

Copy file name to clipboardExpand all lines: custom_components/motion_frontend/alarm_control_panel.py
+25-26Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Support for Motion daemon DVR Alarm Control Panels."""
22

3-
import typing
3+
from typing import TYPE_CHECKING
44

55
import homeassistant.components.alarm_control_panel as alarm_control_panel
66

@@ -38,7 +38,7 @@ class AlarmControlPanelState(StrEnum):
3838
EXTRA_ATTR_LAST_TRIGGERED,
3939
)
4040

41-
if typing.TYPE_CHECKING:
41+
if TYPE_CHECKING:
4242
from homeassistant.config_entries import ConfigEntry
4343
from homeassistant.core import HomeAssistant
4444
from homeassistant.helpers.device_registry import DeviceInfo
@@ -55,9 +55,7 @@ async def async_setup_entry(
5555
)
5656

5757

58-
class MotionFrontendAlarmControlPanel(
59-
alarm_control_panel.AlarmControlPanelEntity
60-
):
58+
class MotionFrontendAlarmControlPanel(alarm_control_panel.AlarmControlPanelEntity):
6159

6260
_attr_should_poll = True
6361
_attr_supported_features = (
@@ -67,17 +65,18 @@ class MotionFrontendAlarmControlPanel(
6765
| alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
6866
)
6967

70-
state: AlarmControlPanelState
71-
code_arm_required: bool
72-
code_format: alarm_control_panel.CodeFormat | None
73-
device_info: "DeviceInfo"
74-
extra_state_attributes: dict
75-
name: str
76-
unique_id: str
68+
if TYPE_CHECKING:
69+
alarm_state: AlarmControlPanelState
70+
code_arm_required: bool
71+
code_format: alarm_control_panel.CodeFormat | None
72+
device_info: DeviceInfo
73+
extra_state_attributes: dict
74+
name: str
75+
unique_id: str
7776

78-
_armmode: AlarmControlPanelState
79-
_disarm_sets: dict[AlarmControlPanelState, frozenset]
80-
_current_disarm_set: frozenset
77+
_armmode: AlarmControlPanelState
78+
_disarm_sets: dict[AlarmControlPanelState, frozenset]
79+
_current_disarm_set: frozenset
8180

8281
DISARM_SET_MAP = {
8382
AlarmControlPanelState.ARMED_HOME: CONF_ALARM_DISARMHOME_CAMERAS,
@@ -87,7 +86,7 @@ class MotionFrontendAlarmControlPanel(
8786
}
8887

8988
__slots__ = (
90-
"state",
89+
"alarm_state",
9190
"code_arm_required",
9291
"code_format",
9392
"device_info",
@@ -128,11 +127,13 @@ def __init__(self, api: "MotionFrontendApi"):
128127
self.unique_id = f"{api.unique_id}_CP"
129128

130129
# try to determine startup state by inspecting cameras setup. This code is pretty slacking
131-
disarmed = {
132-
camera.id for camera in self._api.cameras.values() if camera.paused
133-
}
130+
disarmed = {camera.id for camera in self._api.cameras.values() if camera.paused}
134131
# set this as 'baseline'
135-
self.state = self._armmode = AlarmControlPanelState.DISARMED if disarmed else AlarmControlPanelState.ARMED_AWAY
132+
self.alarm_state = self._armmode = (
133+
AlarmControlPanelState.DISARMED
134+
if disarmed
135+
else AlarmControlPanelState.ARMED_AWAY
136+
)
136137
# then try to infer from the different configured sets
137138
if (
138139
self._pause_disarmed
@@ -142,11 +143,9 @@ def __init__(self, api: "MotionFrontendApi"):
142143
for _state, _disarm_set in self._disarm_sets.items():
143144
if disarmed == _disarm_set:
144145
self._current_disarm_set = _disarm_set
145-
self.state = self._armmode = _state
146+
self.alarm_state = self._armmode = _state
146147
break
147148

148-
149-
150149
async def async_update(self):
151150
"""
152151
this polling is not necessary overall
@@ -220,7 +219,7 @@ def notify_state_changed(self, camera: "MotionFrontendCamera"):
220219

221220
if not camera.connected:
222221
self.extra_state_attributes[EXTRA_ATTR_LAST_PROBLEM] = camera.entity_id
223-
if self.state is not AlarmControlPanelState.TRIGGERED:
222+
if self.alarm_state is not AlarmControlPanelState.TRIGGERED:
224223
# We'll use PENDING to indicate a camera connection problem
225224
self._set_state(AlarmControlPanelState.PENDING)
226225
return
@@ -246,7 +245,7 @@ def _set_armmode(self, state: AlarmControlPanelState) -> None:
246245
self._set_state(state)
247246

248247
def _set_state(self, state: AlarmControlPanelState) -> None:
249-
if self.state != state:
250-
self.state = state
248+
if self.alarm_state != state:
249+
self.alarm_state = state
251250
if self.hass and self.enabled:
252251
self.async_write_ha_state()
Collapse file

‎custom_components/motion_frontend/config_flow.py‎

Copy file name to clipboardExpand all lines: custom_components/motion_frontend/config_flow.py
+10-6Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Config flow to configure Agent devices."""
22

3+
from typing import TYPE_CHECKING
4+
import voluptuous as vol
5+
36
import homeassistant.config_entries as config_entries
47
import homeassistant.const as hac
58
from homeassistant.helpers.aiohttp_client import async_get_clientsession
69
import homeassistant.helpers.config_validation as cv
7-
import voluptuous as vol
10+
811

912
from .const import (
1013
CONF_ALARM_DISARMAWAY_CAMERAS,
@@ -37,7 +40,7 @@
3740

3841
# OptionsFlow: async_step_init
3942
CONF_SELECT_FLOW = "select_flow"
40-
CONF_SELECT_FLOW_OPTIONS = {
43+
CONF_SELECT_FLOW_OPTIONS: dict[str, str | cs.AnyParam] = {
4144
CONF_OPTION_NONE: CONF_OPTION_NONE,
4245
CONF_OPTION_CONNECTION: "Connection",
4346
CONF_OPTION_ALARM: "Alarm Panel",
@@ -222,9 +225,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
222225
def __init__(self, config_entry: config_entries.ConfigEntry):
223226
self._config_entry = config_entry
224227
self._data = dict(config_entry.data)
225-
226228
self._api: MotionHttpClient | None = None # init later since we don't have hass
227-
self._config_set = {} # the actual config(s) of motion cameras
229+
self._config_set: dict[str, str | cs.AnyParam] = (
230+
{}
231+
) # the actual config(s) of motion cameras
228232

229233
self._config_id = "" # camera_id under configuration (async_step_config)
230234
self._config_section = ""
@@ -263,7 +267,7 @@ async def async_step_init(self, user_input=None):
263267
for _id, config in self._api.configs.items()
264268
}
265269

266-
options = dict(CONF_SELECT_FLOW_OPTIONS)
270+
options = CONF_SELECT_FLOW_OPTIONS.copy()
267271
options.update(self._config_set)
268272

269273
return self.async_show_form(
@@ -508,7 +512,7 @@ async def async_step_config(self, user_input=None):
508512
step_id="config",
509513
data_schema=vol.Schema(schema),
510514
description_placeholders={
511-
"camera_id": self._config_set.get(self._config_id),
515+
"camera_id": self._config_set.get(self._config_id), # type: ignore
512516
"config_section": f"<a href='{_get_config_section_url(self._api.version, self._config_section)}'>"
513517
f"{CONF_SELECT_CONFIG_OPTIONS[self._config_section]}</a>",
514518
},

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.