Skip to content

Commit

Permalink
update HA core compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
krahabb committed Apr 8, 2022
1 parent 57576cc commit ff15822
Show file tree
Hide file tree
Showing 8 changed files with 682 additions and 458 deletions.
11 changes: 5 additions & 6 deletions custom_components/motion_frontend/alarm_control_panel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Support for Motion daemon DVR Alarm Control Panels."""
from typing import Any, Mapping
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
FORMAT_TEXT, FORMAT_NUMBER
Expand Down Expand Up @@ -52,7 +51,7 @@ def __init__(self, api: MotionFrontendApi):
self._unique_id = f"{api.unique_id}_CP"
self._name = f"{api.name} Alarm Panel"
self._state = STATE_ALARM_DISARMED
self._extra_attr = {}
self._attr_extra_state_attributes = {}
self._armmode = STATE_ALARM_DISARMED
data = api.config_data.get(CONF_OPTION_ALARM, {})
self._pin: str = data.get(CONF_PIN)
Expand Down Expand Up @@ -144,8 +143,8 @@ def state(self):


@property
def extra_state_attributes(self) -> Mapping[str, Any]:
return self._extra_attr
def extra_state_attributes(self):
return self._attr_extra_state_attributes


async def async_update(self):
Expand Down Expand Up @@ -227,12 +226,12 @@ def notify_state_changed(self, camera: MotionFrontendCamera):
return

if camera.is_triggered:
self._extra_attr[EXTRA_ATTR_LAST_TRIGGERED] = camera.entity_id
self._attr_extra_state_attributes[EXTRA_ATTR_LAST_TRIGGERED] = camera.entity_id
self._set_state(STATE_ALARM_TRIGGERED)
return

if camera.state == STATE_PROBLEM:
self._extra_attr[EXTRA_ATTR_LAST_PROBLEM] = camera.entity_id
self._attr_extra_state_attributes[EXTRA_ATTR_LAST_PROBLEM] = camera.entity_id
if self._state != STATE_ALARM_TRIGGERED:
self._set_state(STATE_PROBLEM) # report camera 'PROBLEM' to this alarm
return
Expand Down
53 changes: 23 additions & 30 deletions custom_components/motion_frontend/camera.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import Any, Mapping
import voluptuous as vol
from datetime import timedelta
import asyncio
from contextlib import closing
from functools import partial
import aiohttp
import async_timeout
import requests
Expand All @@ -17,20 +17,10 @@
async_get_clientsession,
)
from homeassistant.components.camera import (
SUPPORT_STREAM,
Camera,
SUPPORT_STREAM, SUPPORT_ON_OFF,
STATE_IDLE, STATE_RECORDING, STATE_STREAMING
)
from homeassistant.components.camera import (
PLATFORM_SCHEMA, Camera
)
from homeassistant.components.mjpeg.camera import (
#MjpegCamera,
#CONF_MJPEG_URL,CONF_STILL_IMAGE_URL, CONF_VERIFY_SSL,
#CONF_AUTHENTICATION, CONF_USERNAME, CONF_PASSWORD,
#HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION,
filter_urllib3_logging,
)

from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_NAME,
STATE_PAUSED, STATE_PROBLEM, STATE_ALARM_TRIGGERED
Expand Down Expand Up @@ -89,8 +79,6 @@

async def async_setup_entry(hass, config_entry, async_add_entities):

filter_urllib3_logging() # agent_dvr integration does this...

api = hass.data[DOMAIN][config_entry.entry_id]

async_add_entities(api.cameras.values())
Expand Down Expand Up @@ -128,10 +116,9 @@ def __init__(self, client: MotionHttpClient, id: str):
self._recording = False
self._triggered = False
self._state = STATE_PROBLEM if not self.connected else STATE_PAUSED if self.paused else STATE_IDLE
self._extra_attr = {}
self._attr_extra_state_attributes = {}
self._camera_image = None # cached copy
self._available = True

Camera.__init__(self)


Expand Down Expand Up @@ -181,13 +168,13 @@ def state(self) -> StateType:


@property
def is_recording(self):
return self._recording
def extra_state_attributes(self) -> Mapping[str, Any]:
return self._attr_extra_state_attributes


@property
def extra_state_attributes(self) -> Mapping[str, Any]:
return self._extra_attr
def is_recording(self):
return self._recording


@property
Expand All @@ -204,7 +191,9 @@ async def async_will_remove_from_hass(self) -> None:


# override
async def async_camera_image(self):
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
if self.connected:
# only pull the stream/image when the remote is connected
Expand All @@ -215,7 +204,9 @@ async def async_camera_image(self):
image_url = self.image_url
stream_auth_method = self.config.get(cs.STREAM_AUTH_METHOD)
if (image_url is None) or (stream_auth_method == cs.AUTH_MODE_DIGEST):
self._camera_image = await self.hass.async_add_executor_job(self.camera_image)
self._camera_image = await self.hass.async_add_executor_job(
partial(self.camera_image, width=width, height=height)
)
if not self._available:
self._updatestate()
return self._camera_image
Expand Down Expand Up @@ -244,7 +235,9 @@ async def async_camera_image(self):
return self._camera_image


def camera_image(self):
def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""
Return a still image response from the camera.
This is called whenever auth is like DIGEST or
Expand Down Expand Up @@ -309,10 +302,10 @@ async def async_disable_motion_detection(self):
def handle_event(self, data: dict) -> None:
event_id = data.get(EXTRA_ATTR_EVENT_ID)
if event_id:
self._extra_attr[EXTRA_ATTR_EVENT_ID] = event_id
self._attr_extra_state_attributes[EXTRA_ATTR_EVENT_ID] = event_id
filename = data.get(EXTRA_ATTR_FILENAME)
if filename:
self._extra_attr[EXTRA_ATTR_FILENAME] = filename
self._attr_extra_state_attributes[EXTRA_ATTR_FILENAME] = filename

event = data.get("event")
if event == ON_MOVIE_START:
Expand Down Expand Up @@ -342,19 +335,19 @@ def is_triggered(self):
def _settriggered(self, triggered: bool):
if self._triggered != triggered:
self._triggered = triggered
self._extra_attr[EXTRA_ATTR_TRIGGERED] = triggered
self._attr_extra_state_attributes[EXTRA_ATTR_TRIGGERED] = triggered
self._updatestate()


#override MotionCamera
def on_connected_changed(self):
self._extra_attr[EXTRA_ATTR_CONNECTED] = self.connected
self._attr_extra_state_attributes[EXTRA_ATTR_CONNECTED] = self.connected
self._updatestate()


#override MotionCamera
def on_paused_changed(self):
self._extra_attr[EXTRA_ATTR_PAUSED] = self.paused
self._attr_extra_state_attributes[EXTRA_ATTR_PAUSED] = self.paused
self._updatestate()


Expand Down
117 changes: 89 additions & 28 deletions custom_components/motion_frontend/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Config flow to configure Agent devices."""
from types import MappingProxyType
from typing import MappingView
from custom_components.motion_frontend.motionclient.config_schema import Descriptor, Param, SECTION_DATABASE, SECTION_MMALCAM, SECTION_TRACK, SECTION_V4L2
import voluptuous as vol

from homeassistant.config_entries import (
Expand All @@ -8,7 +11,7 @@
from homeassistant.const import (
CONF_HOST, CONF_PORT,
CONF_USERNAME, CONF_PASSWORD,
CONF_PIN, CONF_ARMING_TIME,
CONF_PIN, CONF_ARMING_TIME, CONF_DELAY_TIME
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
Expand All @@ -24,7 +27,7 @@
)
from .const import (
DOMAIN, CONF_PORT_DEFAULT,
CONF_OPTION_NONE,
CONF_OPTION_NONE, CONF_OPTION_UNKNOWN,
CONF_OPTION_CONNECTION, CONF_OPTION_ALARM,
CONF_TLS_MODE, CONF_TLS_MODE_OPTIONS, MAP_TLS_MODE,
CONF_WEBHOOK_MODE, CONF_WEBHOOK_MODE_OPTIONS,
Expand All @@ -50,36 +53,67 @@
CONF_SELECT_CONFIG_OPTIONS = {
CONF_OPTION_NONE: CONF_OPTION_NONE,
cs.SECTION_SYSTEM: "System setup",
cs.SECTION_NETCAM: "Network cameras",
cs.SECTION_STREAM: "Streaming",
cs.SECTION_IMAGE: "Image processing",
cs.SECTION_MOTION: "Motion detection",
cs.SECTION_SCRIPT: "Script / events",
cs.SECTION_PICTURE: "Picture",
cs.SECTION_MOVIE: "Movie",
cs.SECTION_TIMELAPSE: "Timelapse"
cs.SECTION_TIMELAPSE: "Timelapse",
cs.SECTION_NETCAM: "Network cameras",
cs.SECTION_V4L2: "V4L2 cameras",
cs.SECTION_MMALCAM: "Raspberry PI cameras",
cs.SECTION_DATABASE: "Database",
cs.SECTION_TRACK: "Tracking",
CONF_OPTION_UNKNOWN: "Miscellaneous" # add an entry to setup all of the config params we didn't normalize
}

def map_motion_cs_validator(descriptor: cs.Descriptor):
def _map_motion_cs_validator(descriptor: cs.Descriptor):
"""
We're defining schemas in motionclient library bu validators there
are more exotic than HA allows to serialize: provide here a fallback
for unsupported serializations
"""
if descriptor is None:
return str
val = descriptor.validator
if val is cs.validate_userpass:
return str
if isinstance(val, vol.Range):# HA frontend doesnt recognize range of int really well
return int
return val or str
"""
if val in (str, int, bool):
return val
if isinstance(val, vol.In):
return val
#fallback
return str
"""

# link ref to documentation (set of different tags among versions)
SECTION_CONFIG_REF_MAP = {
cs.SECTION_SYSTEM: ("OptDetail_System_Processing", "Options_System_Processing"),
cs.SECTION_V4L2: ("OptDetail_Video4Linux_Devices", "Options_Video4Linux_Devices"),
cs.SECTION_NETCAM: ("OptDetail_Network_Cameras", "Options_Network_Cameras"),
cs.SECTION_MMALCAM: ("OptDetail_Raspi_Cameras", "Options_Raspi_Cameras"),
cs.SECTION_STREAM: ("OptDetail_Stream", "Options_Stream_Webcontrol"),
cs.SECTION_IMAGE: ("OptDetail_Image_Processing", "Options_Image_Processing"),
cs.SECTION_MOTION: ("OptDetail_Motion_Detection", "Options_Motion_Detection"),
cs.SECTION_SCRIPT: ("OptDetail_Scripts", "Options_Scripts"),
cs.SECTION_PICTURE: ("OptDetail_Pictures", "Options_Pictures"),
cs.SECTION_MOVIE: ("OptDetail_Movies", "Options_Movies"),
cs.SECTION_TIMELAPSE: ("OptDetail_Movies", "Options_Movies"),
cs.SECTION_DATABASE: ("OptDetail_Database", "Options_Database"),
cs.SECTION_TRACK: ("OptDetail_Tracking", "Options_Tracking")
}

def _get_config_section_url(version: str, section: str) -> str:
if not version:
version = "3.4.1" # last known older ver
if version.startswith("4.2"):
page = "motion_config.html"
refver = 0
else:
page = "motion_guide.html"
refver = 1
href = SECTION_CONFIG_REF_MAP.get(section)
href = href[refver] if href else "Configuration_OptionsTopic"
return f"https://motion-project.github.io/{version}/{page}#{href}"



class MotionFlowHandler(ConfigFlow, domain=DOMAIN):

Expand Down Expand Up @@ -280,7 +314,7 @@ async def async_step_alarm(self, user_input=None):
cameras = dict(self._config_set)
if len(cameras):
cameras.pop(cs.GLOBAL_ID, None)
else: # add what we know so far...
else: # add what we know so far in case the api is unavailable
cameras.update({_id: _id for _id in data.get(CONF_ALARM_DISARMHOME_CAMERAS)})
cameras.update({_id: _id for _id in data.get(CONF_ALARM_DISARMAWAY_CAMERAS)})
cameras.update({_id: _id for _id in data.get(CONF_ALARM_DISARMNIGHT_CAMERAS)})
Expand Down Expand Up @@ -318,38 +352,65 @@ async def async_step_config(self, user_input=None):
errors = {}

if user_input is not None:
for param, value in user_input.items():
if param == CONF_SELECT_CONFIG:
for key, value in user_input.items():
if key == CONF_SELECT_CONFIG:
self._config_section = value
continue
try:
await self._api.async_config_set(param=param, value=value, id=self._config_id, force=False, persist=False)
await self._api.async_config_set(key=key, value=value, id=self._config_id, force=False, persist=False)
except Exception as e:
errors["base"] = "cannot_connect"
LOGGER.warning("Error (%s) setting motion parameter '%s'", str(e), param)
LOGGER.warning("Error (%s) setting motion parameter '%s'", str(e), key)

if self._config_section == CONF_OPTION_NONE:
return await self.async_step_init()
# else load another schema/options to edit

config_section_set = cs.SECTION_SET_MAP[self._config_section]
config_exclusion_set = cs.CAMERACONFIG_SET if self._config_id == cs.GLOBAL_ID else cs.GLOBALCONFIG_SET
config = self._api.configs.get(self._config_id, {})
schema = {
vol.Optional(param, description={"suggested_value": config.get(param)})
: map_motion_cs_validator(cs.SCHEMA[param])
for param in config_section_set if (param in config) and (param not in config_exclusion_set)
}
config: MappingProxyType[str, cs.Param] = self._api.configs.get(self._config_id, {})
config_section_map: MappingProxyType[str, cs.Descriptor] = cs.SECTION_SET_MAP.get(self._config_section)
schema = {}
if config_section_map:
# expose a set of known motion params normalized through config_schema
config_exclusion_set = cs.CAMERACONFIG_SET if self._config_id == cs.GLOBAL_ID else cs.GLOBALCONFIG_SET
for key, descriptor in config_section_map.items():
if (key in config) and (key not in config_exclusion_set):
param = config.get(key)
schema[vol.Optional(key, description={'suggested_value': param})] = \
_map_motion_cs_validator(descriptor if param is None else param.descriptor)
"""
schema = {
vol.Optional(key, description={'suggested_value': config.get(key)})
: _map_motion_cs_validator(cs.SCHEMA[key])
for key in config_section_set if (key in config) and (key not in config_exclusion_set)
}
"""
else:
# expose any param we didn't normalize
# This isnt working fine since the frontend is not able to render an unknown description
for key in config.keys():
if (key not in cs.SCHEMA.keys()):
param = config.get(key)
schema[vol.Optional(key, description={'suggested_value': param})] = \
str if param is None else _map_motion_cs_validator(param.descriptor)
"""
schema = {
vol.Optional(key, description={'suggested_value': config.get(key)})
: str
for key in config.keys() if (key not in cs.SCHEMA.keys())
}
"""

schema.update({
vol.Required(CONF_SELECT_CONFIG, default = CONF_OPTION_NONE): vol.In(CONF_SELECT_CONFIG_OPTIONS)
})

return self.async_show_form(
step_id="config",
step_id='config',
data_schema=vol.Schema(schema),
description_placeholders={
'camera_id': self._config_set.get(self._config_id),
'config_section': CONF_SELECT_CONFIG_OPTIONS[self._config_section]
'config_section': f"<a href='{_get_config_section_url(self._api.version, self._config_section)}'>" \
f"{CONF_SELECT_CONFIG_OPTIONS[self._config_section]}</a>"
},
errors=errors
)
Loading

0 comments on commit ff15822

Please sign in to comment.