Updated apps
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
"""Camera that loads a picture from a local file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import MailAndPackagesConfigEntry, const
|
||||
from .const import (
|
||||
ATTR_IMAGE_PATH,
|
||||
ATTR_USPS_IMAGE,
|
||||
CAMERA_DATA,
|
||||
CONF_CUSTOM_IMG,
|
||||
CONF_CUSTOM_IMG_FILE,
|
||||
CONF_DURATION,
|
||||
CONF_POST_DE_CUSTOM_IMG,
|
||||
CONF_POST_DE_CUSTOM_IMG_FILE,
|
||||
DOMAIN,
|
||||
SENSOR_NAME,
|
||||
VERSION,
|
||||
)
|
||||
from .utils.image import cleanup_images, generate_delivery_gif, resize_images
|
||||
|
||||
SERVICE_UPDATE_IMAGE = "update_image"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass,
|
||||
config: MailAndPackagesConfigEntry,
|
||||
async_add_entities,
|
||||
):
|
||||
"""Set up the Camera that works with local files."""
|
||||
coordinator = config.runtime_data.coordinator
|
||||
camera = []
|
||||
|
||||
for variable in CAMERA_DATA:
|
||||
temp_cam = MailCam(hass, variable, config, coordinator)
|
||||
camera.append(temp_cam)
|
||||
config.runtime_data.cameras.append(temp_cam)
|
||||
|
||||
async def _update_image(service: ServiceCall) -> None:
|
||||
"""Refresh camera image."""
|
||||
_LOGGER.debug("Updating image: %s", service)
|
||||
cameras = config.runtime_data.cameras
|
||||
entity_id = None
|
||||
|
||||
if ATTR_ENTITY_ID in service.data:
|
||||
entity_id = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
# Update all cameras if no entity_id
|
||||
if entity_id is None:
|
||||
for cam in cameras:
|
||||
await cam.update_file_path()
|
||||
|
||||
else:
|
||||
for cam in cameras:
|
||||
if cam.entity_id in entity_id:
|
||||
await cam.update_file_path()
|
||||
return True
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_IMAGE,
|
||||
_update_image,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_ENTITY_ID): vol.Coerce(str),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
async_add_entities(camera)
|
||||
|
||||
|
||||
class MailCam(CoordinatorEntity, Camera):
|
||||
"""Representation of a local file camera."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
name: str,
|
||||
config: ConfigEntry,
|
||||
coordinator,
|
||||
) -> None:
|
||||
"""Initialize Local File Camera component."""
|
||||
CoordinatorEntity.__init__(self, coordinator)
|
||||
Camera.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self._name = CAMERA_DATA[name][SENSOR_NAME]
|
||||
self._type = name
|
||||
self._host = config.data.get(CONF_HOST)
|
||||
self._unique_id = config.entry_id
|
||||
|
||||
# Derive config keys and default image from camera type name
|
||||
# Remove "_camera" suffix to get base name (e.g., "usps_camera" -> "usps")
|
||||
base_name = self._type.removesuffix("_camera")
|
||||
|
||||
# USPS and Post DE use mail_none.gif, others use no_deliveries_*.jpg
|
||||
if base_name in ("usps", "post_de"):
|
||||
if base_name == "usps":
|
||||
custom_img_key = CONF_CUSTOM_IMG
|
||||
custom_img_file_key = CONF_CUSTOM_IMG_FILE
|
||||
else:
|
||||
custom_img_key = CONF_POST_DE_CUSTOM_IMG
|
||||
custom_img_file_key = CONF_POST_DE_CUSTOM_IMG_FILE
|
||||
default_image = "mail_none.gif"
|
||||
else:
|
||||
# Derive config key names dynamically (e.g., "amazon" -> CONF_AMAZON_CUSTOM_IMG)
|
||||
custom_img_key = getattr(
|
||||
const,
|
||||
f"CONF_{base_name.upper()}_CUSTOM_IMG",
|
||||
None,
|
||||
)
|
||||
custom_img_file_key = getattr(
|
||||
const,
|
||||
f"CONF_{base_name.upper()}_CUSTOM_IMG_FILE",
|
||||
None,
|
||||
)
|
||||
default_image = f"no_deliveries_{base_name}.jpg"
|
||||
|
||||
# Set custom image paths
|
||||
self._no_mail = None
|
||||
if custom_img_key and config.data.get(custom_img_key):
|
||||
self._no_mail = config.data.get(custom_img_file_key)
|
||||
_LOGGER.debug(
|
||||
"%s camera - custom image enabled: %s",
|
||||
self._type,
|
||||
self._no_mail,
|
||||
)
|
||||
|
||||
# Set initial file path based on camera type and custom settings
|
||||
if custom_img_key and config.data.get(custom_img_key):
|
||||
self._file_path = config.data.get(custom_img_file_key)
|
||||
_LOGGER.debug(
|
||||
"%s camera - initial file path set to: %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
else:
|
||||
self._file_path = f"{Path(__file__).parent}/{default_image}"
|
||||
|
||||
# Canonical bundled placeholder for this camera. Always available
|
||||
# regardless of later _file_path changes, so a missing delivery image
|
||||
# can fall back to it instead of returning None (which makes the HA
|
||||
# camera proxy serve HTTP 500).
|
||||
self._default_image_path = f"{Path(__file__).parent}/{default_image}"
|
||||
|
||||
self._cached_image_path: str | None = None
|
||||
self._cached_image_bytes: bytes | None = None
|
||||
self._last_delivery_images: list[str] | None = None
|
||||
self._is_generic: bool = True
|
||||
|
||||
async def async_camera_image(
|
||||
self,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> bytes | None:
|
||||
"""Return image response."""
|
||||
if (
|
||||
self._file_path == self._cached_image_path
|
||||
and self._cached_image_bytes is not None
|
||||
):
|
||||
return self._cached_image_bytes
|
||||
|
||||
_LOGGER.debug(
|
||||
"Camera %s reading image from: %s",
|
||||
self._name,
|
||||
self._file_path,
|
||||
)
|
||||
|
||||
def _read_file(path: str) -> bytes:
|
||||
with Path(path).open("rb") as f:
|
||||
return f.read()
|
||||
|
||||
try:
|
||||
image_bytes = await self.hass.async_add_executor_job(
|
||||
_read_file, self._file_path
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.debug(
|
||||
"Could not read camera %s image from file: %s; "
|
||||
"falling back to bundled placeholder %s",
|
||||
self._name,
|
||||
self._file_path,
|
||||
self._default_image_path,
|
||||
)
|
||||
# Fall back to the bundled placeholder so the camera proxy never
|
||||
# serves HTTP 500 when a delivery image is missing on disk. Skip the
|
||||
# fallback when the primary path already IS the placeholder (true for
|
||||
# non-custom cameras, where _file_path and _default_image_path are
|
||||
# both built from the same default image): re-reading the same
|
||||
# missing file would only fail again, so return None instead.
|
||||
if self._file_path != self._default_image_path:
|
||||
try:
|
||||
image_bytes = await self.hass.async_add_executor_job(
|
||||
_read_file, self._default_image_path
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning(
|
||||
"Camera %s placeholder image also missing: %s",
|
||||
self._name,
|
||||
self._default_image_path,
|
||||
)
|
||||
return None
|
||||
else:
|
||||
# Cache against the placeholder path so repeated reads are
|
||||
# cheap; update_file_path() invalidates the cache when the
|
||||
# primary path changes.
|
||||
self._cached_image_path = self._default_image_path
|
||||
self._cached_image_bytes = image_bytes
|
||||
return image_bytes
|
||||
return None
|
||||
else:
|
||||
self._cached_image_path = self._file_path
|
||||
self._cached_image_bytes = image_bytes
|
||||
return image_bytes
|
||||
|
||||
def check_file_path_access(self, file_path: str) -> None:
|
||||
"""Check that filepath given is readable."""
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.debug(
|
||||
"Could not read camera %s image from file: %s",
|
||||
self._name,
|
||||
file_path,
|
||||
)
|
||||
|
||||
async def update_file_path(self) -> None:
|
||||
"""Update the file_path."""
|
||||
_LOGGER.debug("Camera Update: %s", self._type)
|
||||
_LOGGER.debug("Custom No Mail: %s", self._no_mail)
|
||||
|
||||
if not self.coordinator.last_update_success:
|
||||
_LOGGER.debug("Update to update camera image. Unavailable.")
|
||||
return
|
||||
|
||||
if self.coordinator.data is None:
|
||||
_LOGGER.debug("Unable to update camera image, no data.")
|
||||
return
|
||||
|
||||
if self._type == "usps_camera":
|
||||
self._update_usps_camera()
|
||||
elif self._type == "generic_camera":
|
||||
await self._update_generic_camera()
|
||||
else:
|
||||
await self._update_standard_camera()
|
||||
|
||||
# Invalidate the cache so the next frontend request re-reads the image from disk.
|
||||
# This is necessary because for some shippers (like USPS), the image file path
|
||||
# stays the same but the underlying image content on disk changes.
|
||||
self._cached_image_path = None
|
||||
self._cached_image_bytes = None
|
||||
|
||||
self.check_file_path_access(self._file_path)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_usps_camera(self) -> None:
|
||||
"""Update file path for USPS camera."""
|
||||
self._file_path = f"{Path(__file__).parent}/mail_none.gif"
|
||||
self._is_generic = True
|
||||
required_keys = {ATTR_USPS_IMAGE, ATTR_IMAGE_PATH}
|
||||
if required_keys.issubset(self.coordinator.data):
|
||||
image = self.coordinator.data[ATTR_USPS_IMAGE]
|
||||
path = self.coordinator.data[ATTR_IMAGE_PATH]
|
||||
self._file_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
self._is_generic = not self.coordinator.data.get("usps_update", False)
|
||||
_LOGGER.debug(
|
||||
"usps_camera camera - file path set to: %s",
|
||||
self._file_path,
|
||||
)
|
||||
elif self._no_mail:
|
||||
self._file_path = self._no_mail
|
||||
|
||||
async def _update_generic_camera(self) -> None:
|
||||
"""Update file path for Generic camera."""
|
||||
if self._no_mail:
|
||||
self._file_path = self._no_mail
|
||||
self._is_generic = True
|
||||
_LOGGER.debug("Generic camera - using custom no mail: %s", self._file_path)
|
||||
return
|
||||
|
||||
delivery_images = self._collect_generic_delivery_images()
|
||||
|
||||
if (
|
||||
self._last_delivery_images is not None
|
||||
and delivery_images == self._last_delivery_images
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Generic camera - delivery images unchanged, skipping GIF regeneration"
|
||||
)
|
||||
return
|
||||
|
||||
self._last_delivery_images = delivery_images
|
||||
|
||||
if not delivery_images:
|
||||
self._file_path = f"{Path(__file__).parent}/no_deliveries_generic.jpg"
|
||||
self._is_generic = True
|
||||
_LOGGER.debug(
|
||||
"Generic camera - no deliveries found, using default: %s",
|
||||
self._file_path,
|
||||
)
|
||||
return
|
||||
|
||||
image_path = self.coordinator.data.get(ATTR_IMAGE_PATH, "")
|
||||
full_storage_path = Path(f"{self.hass.config.path()}/{image_path}")
|
||||
gif_path = str(full_storage_path / "generic_deliveries.gif")
|
||||
|
||||
resized_images = await self.hass.async_add_executor_job(
|
||||
resize_images, delivery_images, 800, 600
|
||||
)
|
||||
|
||||
duration = self.config.data.get(CONF_DURATION, 5) * 1000
|
||||
gif_created = await self.hass.async_add_executor_job(
|
||||
generate_delivery_gif,
|
||||
resized_images,
|
||||
gif_path,
|
||||
duration,
|
||||
)
|
||||
|
||||
if gif_created:
|
||||
self._file_path = gif_path
|
||||
self._is_generic = False
|
||||
_LOGGER.debug(
|
||||
"Generic camera - created animated GIF with %d delivery images at %s",
|
||||
len(delivery_images),
|
||||
gif_path,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Failed to create animated GIF, using first delivery image",
|
||||
)
|
||||
self._file_path = delivery_images[0]
|
||||
self._is_generic = False
|
||||
|
||||
for img in resized_images:
|
||||
if await anyio.Path(img).exists():
|
||||
await self.hass.async_add_executor_job(
|
||||
cleanup_images, str(Path(img).parent) + "/", Path(img).name
|
||||
)
|
||||
|
||||
def _collect_generic_delivery_images(self) -> list[str]:
|
||||
"""Collect delivery images for the generic camera."""
|
||||
delivery_images = []
|
||||
enabled_resources = self.config.data.get("resources", [])
|
||||
|
||||
for camera_type in CAMERA_DATA:
|
||||
# Skip generic, USPS, and Post DE cameras
|
||||
if camera_type in ("generic_camera", "usps_camera", "post_de_camera"):
|
||||
continue
|
||||
|
||||
base_name = camera_type.removesuffix("_camera")
|
||||
delivered_key = f"{base_name}_delivered"
|
||||
|
||||
# Check if this shipper's delivery sensor is enabled
|
||||
if delivered_key not in enabled_resources:
|
||||
_LOGGER.debug(
|
||||
"Generic camera - skipping %s (sensor %s not enabled)",
|
||||
base_name,
|
||||
delivered_key,
|
||||
)
|
||||
continue
|
||||
|
||||
# Set image attributes
|
||||
image_attr_name = f"ATTR_{base_name.upper()}_IMAGE"
|
||||
image_attr = getattr(const, image_attr_name, None)
|
||||
path_suffix = f"{base_name}/"
|
||||
no_mail_check = "no_deliveries"
|
||||
|
||||
required_keys = {image_attr, ATTR_IMAGE_PATH}
|
||||
if not required_keys.issubset(self.coordinator.data):
|
||||
continue
|
||||
|
||||
image = self.coordinator.data[image_attr]
|
||||
path = f"{self.coordinator.data[ATTR_IMAGE_PATH]}{path_suffix}"
|
||||
delivery_file_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
|
||||
is_no_mail = image.startswith(
|
||||
no_mail_check,
|
||||
) or self._is_custom_no_mail_image(base_name, delivery_file_path)
|
||||
|
||||
delivery_count_key = f"{base_name}_delivered"
|
||||
has_current_deliveries = (
|
||||
delivery_count_key in self.coordinator.data
|
||||
and self.coordinator.data[delivery_count_key] > 0
|
||||
)
|
||||
|
||||
if (
|
||||
not is_no_mail
|
||||
and Path(delivery_file_path).exists()
|
||||
and has_current_deliveries
|
||||
):
|
||||
delivery_images.append(delivery_file_path)
|
||||
elif is_no_mail:
|
||||
_LOGGER.debug(
|
||||
"Generic camera - filtered out %s no-mail image: %s",
|
||||
base_name,
|
||||
image,
|
||||
)
|
||||
elif not has_current_deliveries:
|
||||
_LOGGER.debug(
|
||||
"Generic camera - filtered out %s (no current deliveries, count=%s): %s",
|
||||
base_name,
|
||||
self.coordinator.data.get(delivery_count_key, 0),
|
||||
image,
|
||||
)
|
||||
|
||||
return delivery_images
|
||||
|
||||
async def _update_standard_camera(self) -> None:
|
||||
"""Update file path for standard cameras (Amazon, UPS, etc)."""
|
||||
base_name = self._type.removesuffix("_camera")
|
||||
if base_name == "post_de":
|
||||
self._file_path = f"{Path(__file__).parent}/mail_none.gif"
|
||||
else:
|
||||
self._file_path = f"{Path(__file__).parent}/no_deliveries_{base_name}.jpg"
|
||||
self._is_generic = True
|
||||
|
||||
if self._no_mail:
|
||||
self._file_path = self._no_mail
|
||||
_LOGGER.debug(
|
||||
"%s camera - using custom no mail: %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
return
|
||||
|
||||
image_attr_name = f"ATTR_{base_name.upper()}_IMAGE"
|
||||
image_attr = getattr(const, image_attr_name, None)
|
||||
|
||||
if not image_attr:
|
||||
return
|
||||
|
||||
required_keys = {image_attr, ATTR_IMAGE_PATH}
|
||||
if not required_keys.issubset(self.coordinator.data):
|
||||
return
|
||||
|
||||
image = self.coordinator.data[image_attr]
|
||||
image_path = self.coordinator.data[ATTR_IMAGE_PATH].rstrip("/") + "/"
|
||||
path = f"{image_path}{base_name}/"
|
||||
coordinator_file_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
|
||||
_LOGGER.debug(
|
||||
"=== %s CAMERA UPDATE === coordinator %s = '%s'",
|
||||
self._type,
|
||||
image_attr,
|
||||
image,
|
||||
)
|
||||
|
||||
# Log all image-related keys in coordinator for this camera
|
||||
all_image_keys = {
|
||||
k: self.coordinator.data.get(k, "NOT SET")
|
||||
for k in self.coordinator.data
|
||||
if "image" in k.lower()
|
||||
}
|
||||
_LOGGER.debug(
|
||||
"%s camera - All image keys in coordinator: %s",
|
||||
self._type,
|
||||
all_image_keys,
|
||||
)
|
||||
|
||||
if await anyio.Path(coordinator_file_path).exists() and os.access(
|
||||
coordinator_file_path,
|
||||
os.R_OK,
|
||||
):
|
||||
self._file_path = coordinator_file_path
|
||||
_LOGGER.debug(
|
||||
"%s camera - found coordinator file: %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
else:
|
||||
await self._find_alternative_image(coordinator_file_path, image)
|
||||
|
||||
# Coordinator hashes the delivery image against the placeholder to detect
|
||||
# whether a real email image was saved; use that result rather than guessing
|
||||
# from file existence (the placeholder may have been copied to the same path).
|
||||
self._is_generic = not self.coordinator.data.get(f"{base_name}_update", False)
|
||||
|
||||
async def _find_alternative_image(
|
||||
self,
|
||||
coordinator_file_path: str,
|
||||
expected_image: str,
|
||||
) -> None:
|
||||
"""Attempt to find an alternative image in the directory."""
|
||||
path_dir = Path(coordinator_file_path).parent
|
||||
_LOGGER.debug(
|
||||
"%s camera - coordinator file not found: %s",
|
||||
self._type,
|
||||
coordinator_file_path,
|
||||
)
|
||||
|
||||
# Define a helper to run blocking I/O in the executor
|
||||
def _scan_images():
|
||||
if not path_dir.exists():
|
||||
_LOGGER.debug(
|
||||
"%s camera - directory does not exist: %s",
|
||||
self._type,
|
||||
path_dir,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
found_images = []
|
||||
for file_path in path_dir.iterdir():
|
||||
if file_path.name.lower().endswith(
|
||||
(".jpg", ".jpeg", ".png", ".gif"),
|
||||
):
|
||||
if file_path.exists() and os.access(file_path, os.R_OK):
|
||||
if "no_deliveries" not in file_path.name:
|
||||
found_images.append(
|
||||
(str(file_path), file_path.stat().st_mtime),
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.debug(
|
||||
"%s camera - error listing directory %s: %s",
|
||||
self._type,
|
||||
path_dir,
|
||||
err,
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return found_images
|
||||
|
||||
# Execute the scan in a background thread
|
||||
image_files = await self.hass.async_add_executor_job(_scan_images)
|
||||
|
||||
if image_files:
|
||||
image_files.sort(key=lambda x: x[1], reverse=True)
|
||||
self._file_path = image_files[0][0]
|
||||
_LOGGER.debug(
|
||||
"%s camera - found alternative image file (most recent): %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
|
||||
def _is_custom_no_mail_image(self, base_name: str, file_path: str) -> bool:
|
||||
"""Check if the given file path is a custom 'no mail' image for the specified camera.
|
||||
|
||||
Args:
|
||||
base_name: The base name of the camera (e.g., 'amazon', 'ups', 'walmart', 'usps')
|
||||
file_path: The full file path to check
|
||||
|
||||
Returns:
|
||||
True if this is a custom 'no mail' image, False otherwise
|
||||
|
||||
"""
|
||||
# Handle USPS camera differently (uses CONF_CUSTOM_IMG instead of CONF_USPS_CUSTOM_IMG)
|
||||
if base_name == "usps":
|
||||
custom_img_key = "CONF_CUSTOM_IMG"
|
||||
custom_img_file_key = "CONF_CUSTOM_IMG_FILE"
|
||||
else:
|
||||
# Handle other cameras (Amazon, UPS, Walmart)
|
||||
custom_img_key = f"CONF_{base_name.upper()}_CUSTOM_IMG"
|
||||
custom_img_file_key = f"CONF_{base_name.upper()}_CUSTOM_IMG_FILE"
|
||||
|
||||
custom_img_conf = getattr(const, custom_img_key, None)
|
||||
custom_img_file_conf = getattr(const, custom_img_file_key, None)
|
||||
|
||||
if (
|
||||
custom_img_conf
|
||||
and custom_img_file_conf
|
||||
and self.config.data.get(custom_img_conf)
|
||||
):
|
||||
custom_file_path = self.config.data.get(custom_img_file_conf)
|
||||
if custom_file_path and Path(custom_file_path).exists():
|
||||
# Check if the file path matches the custom "no mail" image
|
||||
return Path(file_path).resolve() == Path(custom_file_path).resolve()
|
||||
|
||||
return False
|
||||
|
||||
def _get_sensor_name_for_camera(self, camera_type: str) -> str:
|
||||
"""Get the sensor name that corresponds to a camera type.
|
||||
|
||||
Args:
|
||||
camera_type: The camera type (e.g., 'amazon_camera', 'ups_camera', etc.)
|
||||
|
||||
Returns:
|
||||
The corresponding sensor name, or None if no mapping exists
|
||||
|
||||
"""
|
||||
# Remove "_camera" suffix to get base name
|
||||
base_name = camera_type.removesuffix("_camera")
|
||||
|
||||
# Special cases for mail scans cameras
|
||||
if base_name == "usps":
|
||||
return "usps_mail"
|
||||
if base_name == "post_de":
|
||||
return "post_de_mail"
|
||||
|
||||
# For other cameras, use the pattern: {base_name}_delivered
|
||||
return f"{base_name}_delivered"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self.coordinator.data is not None:
|
||||
await self.update_file_path()
|
||||
|
||||
async def async_on_demand_update(self):
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
"""Return device information about the mailbox."""
|
||||
return {
|
||||
"connections": {(DOMAIN, self._unique_id)},
|
||||
"name": self._host,
|
||||
"manufacturer": "IMAP E-Mail",
|
||||
"sw_version": VERSION,
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return f"camera_{self._host}_{self._type}_{self._unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
return {
|
||||
"file_path": self._file_path,
|
||||
"is_generic": self._is_generic,
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.data is not None
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
_LOGGER.debug(
|
||||
"%s camera - coordinator update received, updating file path",
|
||||
self._type,
|
||||
)
|
||||
self.hass.async_create_task(self._async_handle_coordinator_update())
|
||||
|
||||
async def _async_handle_coordinator_update(self) -> None:
|
||||
"""Update file path then write state so the frontend gets the correct image URL."""
|
||||
await self.update_file_path()
|
||||
self.async_write_ha_state()
|
||||
Reference in New Issue
Block a user