-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add camera support #336
base: master
Are you sure you want to change the base?
Add camera support #336
Changes from 5 commits
2478cca
eda0c33
0b88da5
d780d71
4898eca
66d9874
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
"""Tasmota camera.""" | ||
|
||
from __future__ import annotations | ||
|
||
from collections.abc import Awaitable | ||
from dataclasses import dataclass | ||
import logging | ||
from typing import Any | ||
|
||
from aiohttp import ClientResponse, ClientSession | ||
|
||
from .const import CONF_DEEP_SLEEP, CONF_IP, CONF_MAC | ||
from .entity import ( | ||
TasmotaAvailability, | ||
TasmotaAvailabilityConfig, | ||
TasmotaEntity, | ||
TasmotaEntityConfig, | ||
) | ||
from .mqtt import ReceiveMessage | ||
from .utils import ( | ||
config_get_state_offline, | ||
config_get_state_online, | ||
get_topic_command, | ||
get_topic_command_state, | ||
get_topic_stat_result, | ||
get_topic_tele_sensor, | ||
get_topic_tele_state, | ||
get_topic_tele_will, | ||
get_value_by_path, | ||
) | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
@dataclass(frozen=True, kw_only=True) | ||
class TasmotaCameraConfig(TasmotaAvailabilityConfig, TasmotaEntityConfig): | ||
"""Tasmota camera configuation.""" | ||
|
||
idx: int | ||
|
||
command_topic: str | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem to be used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was added earlier to support the configuring the camera parameters. But since it is not used, I have removed it. |
||
result_topic: str | ||
state_topic: str | ||
sensor_topic: str | ||
ip_address: str | ||
|
||
@classmethod | ||
def from_discovery_message(cls, config: dict, platform: str) -> TasmotaCameraConfig: | ||
"""Instantiate from discovery message.""" | ||
return cls( | ||
endpoint="camera", | ||
idx=0, | ||
friendly_name=None, | ||
mac=config[CONF_MAC], | ||
platform=platform, | ||
poll_payload="", | ||
poll_topic=get_topic_command_state(config), | ||
availability_topic=get_topic_tele_will(config), | ||
availability_offline=config_get_state_offline(config), | ||
availability_online=config_get_state_online(config), | ||
deep_sleep_enabled=config[CONF_DEEP_SLEEP], | ||
command_topic=get_topic_command(config), | ||
result_topic=get_topic_stat_result(config), | ||
state_topic=get_topic_tele_state(config), | ||
sensor_topic=get_topic_tele_sensor(config), | ||
ip_address=config[CONF_IP], | ||
) | ||
|
||
|
||
class TasmotaCamera(TasmotaAvailability, TasmotaEntity): | ||
"""Representation of a Tasmota camera.""" | ||
|
||
_cfg: TasmotaCameraConfig | ||
|
||
def __init__(self, **kwds: Any): | ||
"""Initialize.""" | ||
self._sub_state: dict | None = None | ||
super().__init__(**kwds) | ||
|
||
async def subscribe_topics(self) -> None: | ||
"""Subscribe to topics.""" | ||
|
||
def state_message_received(msg: ReceiveMessage) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this used for? The core PR doesn't seem to use it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was part of the original version of the core PR. It was removed during the review. |
||
"""Handle new MQTT state messages.""" | ||
if not self._on_state_callback: | ||
return | ||
|
||
if msg.topic == self._cfg.sensor_topic: | ||
if state := get_value_by_path(msg.payload, ["CAMERA"]): | ||
self._on_state_callback(state) | ||
|
||
availability_topics = self.get_availability_topics() | ||
topics = { | ||
"result_topic": { | ||
"event_loop_safe": True, | ||
"topic": self._cfg.result_topic, | ||
"msg_callback": state_message_received, | ||
}, | ||
"state_topic": { | ||
"event_loop_safe": True, | ||
"topic": self._cfg.state_topic, | ||
"msg_callback": state_message_received, | ||
}, | ||
"sensor_topic": { | ||
"event_loop_safe": True, | ||
"topic": self._cfg.sensor_topic, | ||
"msg_callback": state_message_received, | ||
}, | ||
} | ||
topics = {**topics, **availability_topics} | ||
|
||
self._sub_state = await self._mqtt_client.subscribe( | ||
self._sub_state, | ||
topics, | ||
) | ||
|
||
async def unsubscribe_topics(self) -> None: | ||
"""Unsubscribe to all MQTT topics.""" | ||
self._sub_state = await self._mqtt_client.unsubscribe(self._sub_state) | ||
|
||
def get_still_image_stream( | ||
self, websession: ClientSession | ||
) -> Awaitable[ClientResponse]: | ||
"""Get the io stream to read the static image.""" | ||
still_image_url = f"http://{self._cfg.ip_address}/snapshot.jpg" | ||
emontnemery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return websession.get(still_image_url) | ||
|
||
def get_mjpeg_stream(self, websession: ClientSession) -> Awaitable[ClientResponse]: | ||
"""Get the io stream to read the mjpeg stream.""" | ||
mjpeg_url = f"http://{self._cfg.ip_address}:81/cam.mjpeg" | ||
emontnemery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return websession.get(mjpeg_url) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,11 @@ | |
|
||
from . import config_validation as cv | ||
from .button import TasmotaButtonTrigger, TasmotaButtonTriggerConfig | ||
from .camera import TasmotaCamera, TasmotaCameraConfig | ||
from .const import ( | ||
CONF_BATTERY, | ||
CONF_BUTTON, | ||
CONF_CAM, | ||
CONF_DEEP_SLEEP, | ||
CONF_DEVICENAME, | ||
CONF_FRIENDLYNAME, | ||
|
@@ -121,6 +123,7 @@ | |
CONF_FULLTOPIC: cv.string, | ||
CONF_HOSTNAME: cv.string, | ||
vol.Optional(CONF_IFAN, default=0): cv.bit, # Added in Tasmota 9.0.0.4 | ||
vol.Optional(CONF_CAM, default=0): cv.bit, | ||
CONF_IP: cv.string, | ||
CONF_LIGHT_SUBTYPE: cv.positive_int, | ||
CONF_LINK_RGB_CT: cv.bit, | ||
|
@@ -392,6 +395,21 @@ def get_fan_entities( | |
return fan_entities | ||
|
||
|
||
def get_camera_entities( | ||
discovery_msg: dict, | ||
) -> list[tuple[TasmotaCameraConfig | None, DiscoveryHashType]]: | ||
"""Generate camera configuration.""" | ||
camera_entities: list[tuple[TasmotaCameraConfig | None, DiscoveryHashType]] = [] | ||
|
||
entity = None | ||
discovery_hash = (discovery_msg[CONF_MAC], "cam", "cam", 0) | ||
if CONF_CAM in discovery_msg and discovery_msg[CONF_CAM]: | ||
entity = TasmotaCameraConfig.from_discovery_message(discovery_msg, "camera") | ||
camera_entities.append((entity, discovery_hash)) | ||
|
||
return camera_entities | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move this to before There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
def get_switch_entities( | ||
discovery_msg: dict, | ||
) -> list[tuple[TasmotaRelayConfig | None, DiscoveryHashType]]: | ||
|
@@ -486,6 +504,8 @@ def get_entities_for_platform( | |
entities.extend(get_status_sensor_entities(discovery_msg)) | ||
elif platform == "switch": | ||
entities.extend(get_switch_entities(discovery_msg)) | ||
elif platform == "camera": | ||
entities.extend(get_camera_entities(discovery_msg)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move this before cover to keep the alphabetical sorting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
return entities | ||
|
||
|
||
|
@@ -514,6 +534,8 @@ def get_entity( | |
return TasmotaStatusSensor(config=config, mqtt_client=mqtt_client) | ||
if platform == "switch": | ||
return TasmotaRelay(config=config, mqtt_client=mqtt_client) | ||
if platform == "camera": | ||
return TasmotaCamera(config=config, mqtt_client=mqtt_client) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move this before cover to keep the alphabetical sorting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
return None | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
voluptuous>=0.12.0 | ||
aiohttp>=3.11.12 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does tasmota support more than a single camera? If not, can we skip this, it seems to be hardcoded to 0 below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.