Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion custom_components/opendisplay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from .coordinator import Hub
from .runtime_data import OpenDisplayConfigEntry, OpenDisplayBLERuntimeData
from .services import async_setup_services
from .tag_types import get_tag_types_manager
from .util import is_ble_entry

_LOGGER: Final = logging.getLogger(__name__)
Expand Down
43 changes: 39 additions & 4 deletions custom_components/opendisplay/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
"""Config flow for OpenDisplay integration."""
from __future__ import annotations

from typing import Any, Final, Mapping
from typing import Any, Final
import asyncio

import aiohttp
import voluptuous as vol
from habluetooth.models import BluetoothServiceInfoBleak
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, OptionsFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import CONF_HOST
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import TextSelectorType

from .const import DOMAIN
from .const import (
DOMAIN,
CONF_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
DEFAULT_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
MIN_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
MAX_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
)
from .ble import (
get_protocol_by_manufacturer_id,
BLEConnection,
Expand Down Expand Up @@ -569,6 +574,7 @@ def __init__(self) -> None:
self._button_debounce = 0.5
self._nfc_debounce = 1.0
self._custom_font_dirs = ""
self._deep_sleep_queue_expiry_hours = DEFAULT_DEEP_SLEEP_QUEUE_EXPIRY_HOURS

async def async_step_init(self, user_input=None):
"""Manage OpenDisplay options.
Expand All @@ -589,6 +595,10 @@ async def async_step_init(self, user_input=None):
self._button_debounce = self.config_entry.options.get("button_debounce", 0.5)
self._nfc_debounce = self.config_entry.options.get("nfc_debounce", 1.0)
self._custom_font_dirs = self.config_entry.options.get("custom_font_dirs", "")
self._deep_sleep_queue_expiry_hours = self.config_entry.options.get(
CONF_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
DEFAULT_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
)

# Check if this is a BLE device
entry_data = self.config_entry.runtime_data
Expand All @@ -600,13 +610,26 @@ async def async_step_init(self, user_input=None):

if user_input is not None:
# Update blacklisted tags
deep_sleep_queue_expiry_hours = user_input.get(
CONF_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
DEFAULT_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
)
try:
deep_sleep_queue_expiry_hours = int(deep_sleep_queue_expiry_hours)
except (TypeError, ValueError):
deep_sleep_queue_expiry_hours = DEFAULT_DEEP_SLEEP_QUEUE_EXPIRY_HOURS
deep_sleep_queue_expiry_hours = max(
MIN_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
min(deep_sleep_queue_expiry_hours, MAX_DEEP_SLEEP_QUEUE_EXPIRY_HOURS),
)
return self.async_create_entry(
title="",
data={
"blacklisted_tags": user_input.get("blacklisted_tags", []),
"button_debounce": user_input.get("button_debounce", 0.5),
"nfc_debounce": user_input.get("nfc_debounce", 1.0),
"custom_font_dirs": user_input.get("custom_font_dirs", ""),
CONF_DEEP_SLEEP_QUEUE_EXPIRY_HOURS: deep_sleep_queue_expiry_hours,
}
)

Expand Down Expand Up @@ -672,5 +695,17 @@ async def async_step_init(self, user_input=None):
autocomplete="path"
)
),
vol.Optional(
CONF_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
default=self._deep_sleep_queue_expiry_hours,
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=MIN_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
max=MAX_DEEP_SLEEP_QUEUE_EXPIRY_HOURS,
step=1,
unit_of_measurement="h",
mode=selector.NumberSelectorMode.BOX,
)
),
}),
)
5 changes: 5 additions & 0 deletions custom_components/opendisplay/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
DOMAIN = "opendisplay"
SIGNAL_TAG_UPDATE = f"{DOMAIN}_tag_update"
SIGNAL_TAG_IMAGE_UPDATE = f"{DOMAIN}_tag_image_update"
SIGNAL_TAG_CHECKIN = f"{DOMAIN}_tag_checkin"
SIGNAL_AP_UPDATE = f"{DOMAIN}_ap_update"
CONF_DEEP_SLEEP_QUEUE_EXPIRY_HOURS = "deep_sleep_queue_expiry_hours"
DEFAULT_DEEP_SLEEP_QUEUE_EXPIRY_HOURS = 4
MIN_DEEP_SLEEP_QUEUE_EXPIRY_HOURS = 1
MAX_DEEP_SLEEP_QUEUE_EXPIRY_HOURS = 24
OPENDISPLAY_CONFIG_URL = "https://opendisplay.org/firmware/config/"
ATC_CONFIG_URL = "https://atc1441.github.io/ATC_BLE_OEPL_Image_Upload.html"

Expand Down
70 changes: 65 additions & 5 deletions custom_components/opendisplay/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
from homeassistant.helpers import entity_registry as er
import logging

from .const import DOMAIN, SIGNAL_AP_UPDATE, SIGNAL_TAG_UPDATE, SIGNAL_TAG_IMAGE_UPDATE
from .const import (
DOMAIN,
SIGNAL_AP_UPDATE,
SIGNAL_TAG_CHECKIN,
SIGNAL_TAG_IMAGE_UPDATE,
SIGNAL_TAG_UPDATE,
)
from .tag_types import get_tag_types_manager, get_hw_string

_LOGGER: Final = logging.getLogger(__name__)
Expand Down Expand Up @@ -516,10 +522,13 @@ async def _handle_tag_message(self, tag_data: dict) -> None:
Args:
tag_data: Dictionary containing tag properties from the AP
"""
tag_mac = tag_data.get("mac")
tag_mac = self._normalize_tag_mac(tag_data.get("mac"))
if not tag_mac:
return

tag_data = dict(tag_data)
tag_data["mac"] = tag_mac

# Process tag data
is_new_tag = await self._process_tag_data(tag_mac, tag_data)
# Save to storage if this was a new tag
Expand Down Expand Up @@ -698,6 +707,8 @@ async def _process_tag_data(self, tag_mac: str, tag_data: dict, is_initial_load:

# Fire state update event
async_dispatcher_send(self.hass, f"{SIGNAL_TAG_UPDATE}_{tag_mac}")
if not is_initial_load:
async_dispatcher_send(self.hass, SIGNAL_TAG_CHECKIN, tag_mac)

# Handle wakeup event if needed and not initial load
wakeup_reason = tag_data.get("wakeupReason")
Expand Down Expand Up @@ -850,7 +861,10 @@ async def _fetch_all_tags_from_ap(self) -> dict:
# Add tags to set
for tag in data.get("tags", []):
if "mac" in tag:
result[tag["mac"]] = tag
normalized_mac = self._normalize_tag_mac(tag["mac"])
tag_copy = dict(tag)
tag_copy["mac"] = normalized_mac
result[normalized_mac] = tag_copy

# Check for pagination
if "continu" in data and data["continu"] > 0:
Expand Down Expand Up @@ -1448,10 +1462,11 @@ def is_tag_online(self, tag_mac: str) -> bool:
Returns:
True if the tag is online, False if timed out or not found.
"""
if tag_mac not in self.tags:
normalized_mac = self._normalize_tag_mac(tag_mac)
if normalized_mac not in self.tags:
return False

tag_data = self.get_tag_data(tag_mac)
tag_data = self.get_tag_data(normalized_mac)
last_seen = tag_data.get("last_seen", 0)

if last_seen == 0:
Expand All @@ -1467,3 +1482,48 @@ def is_tag_online(self, tag_mac: str) -> bool:
current_time = datetime.now(timezone.utc).timestamp()

return (current_time - last_seen) < timeout_threshold

@staticmethod
def _normalize_tag_mac(tag_mac: str | None) -> str | None:
"""Normalize tag MAC address to uppercase."""
if tag_mac is None:
return None
return tag_mac.upper()

def is_tag_in_deep_sleep(self, tag_mac: str) -> bool:
"""Return whether the tag is configured for deep sleep."""
normalized_mac = self._normalize_tag_mac(tag_mac)
if normalized_mac is None:
return False

tag_data = self.get_tag_data(normalized_mac)
modecfgjson = tag_data.get("modecfgjson")
if not isinstance(modecfgjson, dict):
return False

if bool(modecfgjson.get("deepsleep")):
return True

maxsleep = modecfgjson.get("maxsleep")
if isinstance(maxsleep, (int, float)) and maxsleep >= 15:
return True

return False

def is_tag_currently_sleeping(self, tag_mac: str) -> bool:
"""Return whether the tag is currently sleeping."""
normalized_mac = self._normalize_tag_mac(tag_mac)
if normalized_mac is None:
return False

tag_data = self.get_tag_data(normalized_mac)
next_checkin = tag_data.get("next_checkin")
if not isinstance(next_checkin, (int, float)) or next_checkin <= 0:
return False

current_time = datetime.now(timezone.utc).timestamp()
return current_time < next_checkin

def should_queue_image_upload(self, tag_mac: str) -> bool:
"""Return whether image upload should be queued for this tag."""
return self.is_tag_in_deep_sleep(tag_mac) and self.is_tag_currently_sleeping(tag_mac)
1 change: 0 additions & 1 deletion custom_components/opendisplay/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
CONF_PLATFORM,
CONF_TYPE,
)
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN

_LOGGER: Final = logging.getLogger(__name__)
Expand Down
1 change: 0 additions & 1 deletion custom_components/opendisplay/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .coordinator import Hub
from .runtime_data import OpenDisplayConfigEntry, OpenDisplayBLERuntimeData

Expand Down
2 changes: 1 addition & 1 deletion custom_components/opendisplay/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,4 @@ def device_info(self) -> DeviceInfo:
@property
def available(self) -> bool:
"""Return if the entity is available."""
return bluetooth.async_address_present(self.hass, self._mac_address)
return bluetooth.async_address_present(self.hass, self._mac_address, connectable=False)
3 changes: 0 additions & 3 deletions custom_components/opendisplay/imagegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .registry import get_all_handlers

# Import handler modules to trigger decorator registration
from . import text, shapes, icons, media, visualizations, debug

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -222,7 +221,6 @@ async def get_ble_tag_info(self, hass: HomeAssistant, entity_id: str) -> tuple[i
if runtime_data is not None and isinstance(runtime_data, OpenDisplayBLERuntimeData):
if runtime_data.mac_address.upper() == tag_mac:
device_metadata = runtime_data.device_metadata
protocol_type = runtime_data.protocol_type
break

if not device_metadata:
Expand All @@ -236,7 +234,6 @@ async def get_ble_tag_info(self, hass: HomeAssistant, entity_id: str) -> tuple[i
metadata = BLEDeviceMetadata(device_metadata)

# Extract device capabilities
hw_type = metadata.hw_type
width = metadata.width
height = metadata.height

Expand Down
4 changes: 2 additions & 2 deletions custom_components/opendisplay/imagegen/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async def draw_polygon(ctx: DrawingContext, element: dict) -> None:
# Get polygon properties
fill = ctx.colors.resolve(element.get("fill"))
outline = ctx.colors.resolve(element.get("outline", "black"))
width = element.get("width", 1)
element.get("width", 1)

# Draw the polygon
draw.polygon(vertices, fill=fill, outline=outline)
Expand Down Expand Up @@ -346,7 +346,7 @@ def draw_dashed_line(draw: ImageDraw.ImageDraw,
if dash_end >= line_length:
# A partial dash exists that ends exactly or beyond the line_end
dash_end = line_length
segment_len = dash_end - current_pos
dash_end - current_pos

segment_start_x = x1 + step_x * current_pos
segment_start_y = y1 + step_y * current_pos
Expand Down
4 changes: 2 additions & 2 deletions custom_components/opendisplay/imagegen/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async def draw_multiline(ctx: DrawingContext, element: dict) -> None:

for segment in segments:
color = ctx.colors.resolve(segment.color)
bbox = draw.textbbox(
draw.textbbox(
(segment.start_x, current_y),
segment.text,
font=font,
Expand All @@ -242,7 +242,7 @@ async def draw_multiline(ctx: DrawingContext, element: dict) -> None:
stroke_fill=stroke_fill
)
else:
bbox = draw.textbbox(
draw.textbbox(
(x, current_y),
str(line),
font=font,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/opendisplay/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
PARALLEL_UPDATES = 1

from homeassistant.components.select import SelectEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
Expand Down
1 change: 0 additions & 1 deletion custom_components/opendisplay/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
from .runtime_data import OpenDisplayConfigEntry
from .const import DOMAIN
from .util import is_ble_entry
from .tag_types import get_hw_string, get_hw_dimensions

_LOGGER: Final = logging.getLogger(__name__)

Expand Down
Loading
Loading