Skip to content

Commit

Permalink
Merge pull request #11 from timniklas/timniklas-patch-1
Browse files Browse the repository at this point in the history
improved camera stream handling
  • Loading branch information
timniklas authored Oct 30, 2024
2 parents 3dfd1fa + 209b613 commit 59eee02
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 35 deletions.
33 changes: 33 additions & 0 deletions custom_components/ilifestyle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN
from .coordinator import MqttCoordinator

PLATFORMS: list[Platform] = [Platform.CAMERA, Platform.BUTTON]

@dataclass
class RuntimeData:
"""Class to hold your data."""

coordinator: DataUpdateCoordinator
cancel_update_listener: Callable

async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Integration from a config entry."""

hass.data.setdefault(DOMAIN, {})

# Initialise the coordinator that manages data updates from your api.
# This is defined in coordinator.py
coordinator = MqttCoordinator(hass, config_entry)

# Perform an initial data load from api.
# async_config_entry_first_refresh() is special in that it does not log errors if it fails
await coordinator.async_config_entry_first_refresh()

# Initialise a listener for config flow options changes.
# See config_flow for defining an options setting that shows up as configure on the integration.
cancel_update_listener = config_entry.add_update_listener(_async_update_listener)

# Add the coordinator and update listener to hass data to make
hass.data[DOMAIN][config_entry.entry_id] = RuntimeData(
coordinator, cancel_update_listener
)

# Setup platforms (based on the list of entity types in PLATFORMS defined above)
# This calls the async_setup method in each of your entity type files.
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS);
Expand All @@ -37,5 +66,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry, PLATFORMS
)

# Remove the config entry from the hass data object.
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)

# Return that unloading was successful.
return unload_ok
89 changes: 62 additions & 27 deletions custom_components/ilifestyle/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
from dataclasses import dataclass

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.button import ButtonEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.device_registry import DeviceInfo

_LOGGER = logging.getLogger(__name__)

from .mqtt import LifestyleMqtt
from .coordinator import MqttCoordinator
from .const import DOMAIN
from homeassistant.const import (
CONF_DEVICE_ID,
Expand All @@ -25,62 +25,97 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
):
"""Set up a Buttons."""
# This gets the data update coordinator from hass.data as specified in your __init__.py
coordinator: MqttCoordinator = hass.data[DOMAIN][
config_entry.entry_id
].coordinator

async_add_entities([
CallButton(hass, config_entry),
OpenButton(hass, config_entry)
CallButton(config_entry, coordinator),
HangupButton(config_entry, coordinator),
OpenButton(config_entry, coordinator)
], True)

class CallButton(ButtonEntity):
class CallButton(CoordinatorEntity):

_attr_has_entity_name = True
_attr_translation_key = "callbutton"
_attr_icon = "mdi:video"

def __init__(self, hass, config_entry):
def __init__(self, config_entry: ConfigEntry, coordinator: MqttCoordinator):
"""Initialize."""
super().__init__()
deviceid = config_entry.data[CONF_DEVICE_ID]
token = config_entry.data[CONF_TOKEN]
self._mqtt_client = LifestyleMqtt(mqtt_username=deviceid, mqtt_password=token, alias=self._attr_translation_key)
self.unique_id = f"{deviceid}-{self._attr_translation_key}"
super().__init__(coordinator)
self.unique_id = f"{config_entry.data[CONF_DEVICE_ID]}-{self._attr_translation_key}"
self.device_info = DeviceInfo(
identifiers={(DOMAIN, config_entry.data[CONF_DEVICE_ID])}
)

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()

async def _async_press_action(self) -> None:
"""Handle the button press."""
await self.coordinator.call_door()

@property
def available(self) -> bool:
"""Return the state of the sensor."""
return self.coordinator.data.connected

class HangupButton(CoordinatorEntity):

_attr_has_entity_name = True
_attr_translation_key = "hangupbutton"
_attr_icon = "mdi:video-off"

def __init__(self, config_entry: ConfigEntry, coordinator: MqttCoordinator):
"""Initialize."""
super().__init__(coordinator)
self.unique_id = f"{config_entry.data[CONF_DEVICE_ID]}-{self._attr_translation_key}"
self.device_info = DeviceInfo(
identifiers={(DOMAIN, deviceid)}
identifiers={(DOMAIN, config_entry.data[CONF_DEVICE_ID])}
)
self._mqtt_client.connect()

async def async_press(self) -> None:
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()

async def _async_press_action(self) -> None:
"""Handle the button press."""
await self._mqtt_client.call_door()
await self.coordinator.hangup_door()

@property
def available(self) -> bool:
"""Return the state of the sensor."""
return self._mqtt_client.connected
return self.coordinator.data.connected

class OpenButton(ButtonEntity):
class OpenButton(CoordinatorEntity):

_attr_has_entity_name = True
_attr_translation_key = "openbutton"
_attr_icon = "mdi:door-open"

def __init__(self, hass, config_entry):
def __init__(self, config_entry: ConfigEntry, coordinator: MqttCoordinator):
"""Initialize."""
super().__init__()
deviceid = config_entry.data[CONF_DEVICE_ID]
token = config_entry.data[CONF_TOKEN]
self._mqtt_client = LifestyleMqtt(mqtt_username=deviceid, mqtt_password=token, alias=self._attr_translation_key)
super().__init__(coordinator)
self.unique_id = f"{config_entry.data[CONF_DEVICE_ID]}-{self._attr_translation_key}"
self.device_info = DeviceInfo(
identifiers={(DOMAIN, config_entry.data[CONF_DEVICE_ID])}
)
self._mqtt_client.connect()

async def async_press(self) -> None:
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()

async def _async_press_action(self) -> None:
"""Handle the button press."""
await self._mqtt_client.open_door()
await self.coordinator.open_door()

@property
def available(self) -> bool:
"""Return the state of the sensor."""
return self._mqtt_client.connected
return self.coordinator.data.connected
38 changes: 33 additions & 5 deletions custom_components/ilifestyle/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
CameraEntityFeature,
)
from homeassistant.components.ffmpeg import get_ffmpeg_manager
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .coordinator import MqttCoordinator
from .const import DOMAIN
from homeassistant.const import (
CONF_DEVICE_ID,
Expand All @@ -30,19 +32,25 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
):
"""Set up a Camera."""
async_add_entities([LifestyleCamera(hass, config_entry)], True)
# This gets the data update coordinator from hass.data as specified in your __init__.py
coordinator: MqttCoordinator = hass.data[DOMAIN][
config_entry.entry_id
].coordinator

async_add_entities([LifestyleCamera(hass, config_entry, coordinator)], True)

class LifestyleCamera(Camera):

class LifestyleCamera(CoordinatorEntity, Camera):

_attr_has_entity_name = True
_attr_translation_key = "video"
_attr_supported_features = CameraEntityFeature.STREAM
_options = "-pred 1"

def __init__(self, hass, config_entry):
def __init__(self, hass, config_entry, coordinator):
"""Initialize."""
super().__init__()
super().__init__(coordinator)
Camera.__init__(self)
self._manager = get_ffmpeg_manager(hass)
self._url = config_entry.data[CONF_URL]
self.unique_id = f"{config_entry.data[CONF_DEVICE_ID]}-{self._attr_translation_key}"
Expand All @@ -56,6 +64,26 @@ def __init__(self, hass, config_entry):
identifiers={(DOMAIN, config_entry.data[CONF_DEVICE_ID])}
)

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()

@property
def available(self) -> bool:
"""Return the state of the sensor."""
return self.coordinator.data.connected

@property
def is_streaming(self) -> bool:
"""Return the state of the sensor."""
return self.coordinator.data.transmitting

@property
def is_on(self) -> bool:
"""Return the state of the sensor."""
return self.coordinator.data.transmitting

async def stream_source(self) -> str:
"""Return the stream source."""
return self._url.split(" ")[-1]
Expand Down
86 changes: 86 additions & 0 deletions custom_components/ilifestyle/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from dataclasses import dataclass
from datetime import timedelta
import logging

import re
import asyncio

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_TOKEN
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN
from .mqtt import LifestyleMqtt

_LOGGER = logging.getLogger(__name__)

@dataclass
class MqttData:
"""Class to hold api data."""

connected: bool
transmitting: bool

class MqttCoordinator(DataUpdateCoordinator):
"""My coordinator."""

def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize coordinator."""

# Set variables from values entered in config flow setup
deviceid = config_entry.data[CONF_DEVICE_ID]
token = config_entry.data[CONF_TOKEN]

self.mqtt_client = LifestyleMqtt(mqtt_username=deviceid, mqtt_password=token, callback=self.updateData)
self.transmitting = False

# Initialise DataUpdateCoordinator
super().__init__(
hass,
_LOGGER,
name=f"{DOMAIN} ({config_entry.unique_id})",
# Set update method to get devices on first load.
update_method=self.async_update_data,
# Do not set a polling interval as data will be pushed.
# You can remove this line but left here for explanatory purposes.
update_interval=None,
)

def updateData(self):
self.async_set_updated_data(MqttData(self.mqtt_client.connected, self.transmitting))

async def _transmit(self, duration: int = 60):
await self._updateTransmission(True)
await asyncio.sleep(duration)
await self._updateTransmission(False)

async def _updateTransmission(self, state: bool):
self.transmitting = state
self.updateData()

async def async_update_data(self):
"""Fetch data from API endpoint.
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
if self.mqtt_client.connected == False:
self.mqtt_client.connect()

return MqttData(self.mqtt_client.connected, self.transmitting)

async def call_door(self, duration: int = 60):
await self.mqtt_client.call_door(duration)
await self._transmit(duration)

async def open_door(self):
await self.mqtt_client.open_door()
await self._updateTransmission(False)

async def hangup_door(self):
await self.mqtt_client.hangup_door()
await self._updateTransmission(False)
9 changes: 7 additions & 2 deletions custom_components/ilifestyle/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
class LifestyleMqtt:
"""Class for MQTT client."""

def __init__(self, mqtt_username: str, mqtt_password: str, alias: str = "HomeAssistant") -> None:
def __init__(self, mqtt_username: str, mqtt_password: str, callback, alias: str = "HomeAssistant") -> None:
"""Initialise."""
self.callback = callback
self.connected = False
self._topic = mqtt_username
self._client = mqtt.Client(client_id = alias + "|" + mqtt_username, protocol = mqtt.MQTTv5, transport = "tcp")
Expand All @@ -26,10 +27,11 @@ def _on_connect(self, client, userdata, flags, reason_code, properties=None):
else:
self.connected = False
_LOGGER.error("Connection to iLifestyle MQTT Broker failed:", rc)
self.callback()

def _on_disconnect(self, client, userdata, flags, reason_code, properties=None):
self.connected = False
self.connect()
self.callback()

def connect(self):
if self.connected == False:
Expand All @@ -46,6 +48,9 @@ async def _publish(self, data: str):
async def call_door(self, duration: int = 60):
return await self._publish('{"action": "monitor","ctrl":"1","key_index": 1,"duration":' + str(duration) + '}')

async def hangup_door(self, duration: int = 60):
return await self._publish('{"action": "monitor","ctrl":"F","key_index": 1,"duration":1}')

async def open_door(self):
return await self._publish('{"action": "OPEN DOOR"}')

Expand Down
Loading

0 comments on commit 59eee02

Please sign in to comment.