Skip to content
Open
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: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,4 @@ custom_components/opendisplay/*.jpg
custom_components/opendisplay/lastapinteraction.txt

tests/drawcustom/test_images/rename_me.png
.worktrees/
30 changes: 4 additions & 26 deletions custom_components/opendisplay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from homeassistant.helpers import entity_registry as er, device_registry as dr, storage
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.helpers.typing import ConfigType
from .ble import BLEDeviceMetadata
from .metadata import BLEDeviceMetadata
from .const import DOMAIN
from .coordinator import Hub
from .runtime_data import OpenDisplayConfigEntry, OpenDisplayBLERuntimeData
Expand Down Expand Up @@ -114,7 +114,7 @@ async def async_remove_invalid_ble_entities(
mac_address = entry.data.get("mac_address", "")

# Check power mode - remove battery sensors if not battery/solar powered
from .ble import BLEDeviceMetadata
from .metadata import BLEDeviceMetadata
metadata = BLEDeviceMetadata(device_metadata)
if metadata.power_mode not in (1, 3): # Not battery (1) or solar (3)
for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id):
Expand Down Expand Up @@ -308,7 +308,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenDisplayConfigEntry)
mac_address = entry.data.get("mac_address")
name = entry.data.get("name")
device_metadata = entry.data.get("device_metadata", {})
protocol_type = entry.data.get("protocol_type", "atc") # Default to ATC for backward compatibility
protocol_type = "open_display"

# Get protocol handler for this device
protocol = get_protocol_by_name(protocol_type)
Expand Down Expand Up @@ -378,24 +378,7 @@ def _ble_device_found(
_LOGGER.debug("Failed to parse advertising data for %s: %s", mac_address, err, exc_info=True)
return

# Dynamically update device attributes (skip OpenDisplay fw to avoid incorrect value)
if advertising_data.fw_version and protocol_type != "open_display":
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, f"ble_{mac_address}")}
)
new_fw_string = str(advertising_data.fw_version)
if device_entry and device_entry.sw_version != new_fw_string:
_LOGGER.debug(
"Device %s firmware updated from %s to %s",
mac_address,
device_entry.sw_version,
new_fw_string,
)
device_registry.async_update_device(
device_entry.id,
sw_version=new_fw_string
)


# Build sensor data
sensor_data = {
Expand All @@ -410,11 +393,6 @@ def _ble_device_found(
for sensor in ble_data.sensors.values():
sensor.update_from_advertising_data(sensor_data)

# Remove deprecated clock mode button entities
removed_clock_buttons = await async_remove_clock_mode_buttons(hass, entry)
if removed_clock_buttons:
_LOGGER.info("Removed deprecated clock mode buttons: %s", removed_clock_buttons)

# Remove invalid entities based on the current device config
removed_invalid = await async_remove_invalid_ble_entities(hass, entry, device_metadata)
if removed_invalid:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/opendisplay/ble/image_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from .exceptions import BLEError
from .image_processing import process_image_for_device
from .metadata import BLEDeviceMetadata
from ..metadata import BLEDeviceMetadata

_LOGGER = logging.getLogger(__name__)

Expand Down
20 changes: 9 additions & 11 deletions custom_components/opendisplay/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,22 +443,20 @@ def __init__(self,

async def async_press(self) -> None:
"""Re-interrogate device and update configuration."""
from .ble import BLEConnection, get_protocol_by_name
import opendisplay
from .ble.tlv_parser import config_to_dict, generate_model_name
from homeassistant.helpers import device_registry as dr

_LOGGER.info("Refreshing configuration for OpenDisplay device %s", self._mac_address)

try:
# Get protocol handler
protocol = get_protocol_by_name(self._protocol_type)
fw_info = None

# Connect and interrogate device
async with BLEConnection(self.hass, self._mac_address, self._service_uuid, protocol) as conn:
capabilities = await protocol.interrogate_device(conn)
async with opendisplay.OpenDisplayDevice(mac_address=self._mac_address) as conn:
capabilities = await conn.interrogate()
try:
fw_info = await protocol.read_firmware_version(conn)
fw_info = await conn.read_firmware_version()
except Exception as fw_err:
_LOGGER.warning(
"Failed to read firmware version for %s: %s",
Expand All @@ -473,7 +471,7 @@ async def async_press(self) -> None:
)

# Get updated config from protocol
config = protocol._last_config
config = conn.config

if not config:
raise HomeAssistantError(
Expand All @@ -486,10 +484,10 @@ async def async_press(self) -> None:
"open_display_config": config_to_dict(config),
}
if fw_info:
new_metadata["fw_version"] = fw_info.get("version")
new_metadata["fw_version_raw"] = fw_info.get("raw")
if fw_info.get("sha"):
new_metadata["fw_sha"] = fw_info["sha"]
new_metadata["fw_version"] = f"{fw_info['major']}.{fw_info['minor']}"
new_metadata["fw_version_major"] = fw_info["major"]
new_metadata["fw_version_minor"] = fw_info["minor"]
new_metadata["fw_sha"] = fw_info["sha"]
elif "fw_version" in self._device_metadata:
# Preserve the previously known firmware version if read fails
new_metadata["fw_version"] = self._device_metadata.get("fw_version")
Expand Down
138 changes: 32 additions & 106 deletions custom_components/opendisplay/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@
from .const import DOMAIN
from .ble import (
get_protocol_by_manufacturer_id,
BLEConnection,
UnsupportedProtocolError,
ConfigValidationError,
BLEConnectionError,
BLEProtocolError,
)
import opendisplay
from .tag_types import get_tag_types_manager, get_hw_string
from .util import is_ble_entry
import logging
Expand All @@ -43,8 +41,6 @@ def _format_ble_protocol_label(protocol_type: str) -> str:
"""Return a user-facing label for a BLE protocol."""
if protocol_type == "open_display":
return "OpenDisplay (OD)"
if protocol_type == "atc":
return "OEPL / ATC"
return protocol_type


Expand Down Expand Up @@ -75,15 +71,6 @@ def _bluetooth_description_placeholders(
"""Build placeholders for the Bluetooth confirmation dialog."""
device = self._discovered_device
advertised_details = ""
if device["protocol_type"] == "atc":
battery = f"{device['battery_mv']/1000:.2f}V" if device["battery_mv"] > 0 else "Unknown"
fw_version = str(device["fw_version"]) if device["fw_version"] > 0 else "Unknown"
config_version = str(device["version"]) if device["version"] > 0 else "Unknown"
advertised_details = (
f"\n- Battery: {battery}"
f"\n- Firmware: {fw_version}"
f"\n- Config Version: {config_version}"
)

placeholders = {
"name": device["name"],
Expand Down Expand Up @@ -223,9 +210,9 @@ async def async_step_bluetooth(
manufacturer_id = None
manufacturer_data = b''

# Check for known manufacturer IDs (ATC: 4919, OpenDisplay: 9286)
# Check for known manufacturer IDs (OpenDisplay: 9286)
for mfg_id, mfg_data in discovery_info.manufacturer_data.items():
if mfg_id in (4919, 9286):
if mfg_id == 9286:
manufacturer_id = mfg_id
manufacturer_data = mfg_data
break
Expand Down Expand Up @@ -285,24 +272,17 @@ async def async_step_bluetooth_confirm(

try:
# Get protocol handler for this device
protocol = get_protocol_by_manufacturer_id(
9286 if self._discovered_device["protocol_type"] == "open_display" else 4919
)
protocol = get_protocol_by_manufacturer_id(9286)

# Interrogate device using protocol-specific method
fw_info: dict[str, Any] | None = None

async with BLEConnection(
self.hass,
self._discovered_device["address"],
protocol.service_uuid,
protocol
) as conn:
capabilities = await protocol.interrogate_device(conn)
async with opendisplay.OpenDisplayDevice(mac_address=self._discovered_device["address"]) as conn:
capabilities = await conn.interrogate()
# OpenDisplay devices expose firmware version via 0x0043
if self._discovered_device["protocol_type"] == "open_display":
try:
fw_info = await protocol.read_firmware_version(conn)
fw_info = await conn.read_firmware_version()
except Exception as fw_err:
_LOGGER.warning(
"Failed to read firmware version for %s: %s",
Expand All @@ -322,93 +302,39 @@ async def async_step_bluetooth_confirm(
# Generate model name based on protocol type
hw_type = self._discovered_device["hw_type"]

if self._discovered_device["protocol_type"] == "open_display":
# OpenDisplay devices: Store complete config, generate model name from DisplayConfig
from .ble.tlv_parser import config_to_dict, generate_model_name

if hasattr(protocol, '_last_config') and protocol._last_config:
# Store complete OpenDisplay config for future use
device_metadata = {
"open_display_config": config_to_dict(protocol._last_config),
}
if fw_info:
device_metadata["fw_version"] = fw_info.get("version")
device_metadata["fw_version_raw"] = fw_info.get("raw")
if fw_info.get("sha"):
device_metadata["fw_sha"] = fw_info["sha"]

# Generate model name from display config
if protocol._last_config.displays:
model_name = generate_model_name(protocol._last_config.displays[0])
device_metadata["model_name"] = model_name
_LOGGER.debug("Generated model name from config: %s", model_name)
else:
_LOGGER.warning("OpenDisplay config has no display config")
# OpenDisplay devices: Store complete config, generate model name from DisplayConfig
from .ble.tlv_parser import config_to_dict, generate_model_name

if hasattr(protocol, '_last_config') and protocol._last_config:
# Store complete OpenDisplay config for future use
device_metadata = {
"open_display_config": config_to_dict(protocol._last_config),
}
if fw_info:
device_metadata["fw_version"] = f"{fw_info['major']}.{fw_info['minor']}"
device_metadata["fw_version_major"] = fw_info["major"]
device_metadata["fw_version_minor"] = fw_info["minor"]
device_metadata["fw_sha"] = fw_info["sha"]

# Generate model name from display config
if protocol._last_config.displays:
model_name = generate_model_name(protocol._last_config.displays[0])
device_metadata["model_name"] = model_name
_LOGGER.debug("Generated model name from config: %s", model_name)
else:
# Fallback if config unavailable (shouldn't happen for OpenDisplay)
model_name = get_hw_string(hw_type) if hw_type else "Unknown"
_LOGGER.warning("OpenDisplay config unavailable, using tagtypes fallback: %s", model_name)
# Store individual fields as fallback
device_metadata = {
"hw_type": hw_type,
"fw_version": self._discovered_device["fw_version"],
"width": capabilities.width,
"height": capabilities.height,
"rotatebuffer": capabilities.rotatebuffer,
"color_scheme": capabilities.color_scheme,
"model_name": model_name,
}
_LOGGER.warning("OpenDisplay config has no display config")
else:
# ATC devices: Use tagtypes.json lookup and store individual fields
# Try to get tag types manager, but don't fail if unavailable
tag_types_manager = None
try:
tag_types_manager = await get_tag_types_manager(self.hass)
_LOGGER.debug("Tag types manager loaded successfully")
except Exception as tag_err:
_LOGGER.warning(
"Could not load tag types during config flow, will use fallback values: %s",
tag_err
)

# Fallback if config unavailable (shouldn't happen for OpenDisplay)
model_name = get_hw_string(hw_type) if hw_type else "Unknown"
_LOGGER.debug("Resolved hw_type %s to model: %s", hw_type, model_name)

# Refine color_scheme using TagTypes db if available
if tag_types_manager and tag_types_manager.is_in_hw_map(hw_type):
tag_type = await tag_types_manager.get_tag_info(hw_type)
color_table = tag_type.color_table

if 'yellow' in color_table and 'red' in color_table:
color_scheme = 3 # BWRY
elif 'yellow' in color_table:
color_scheme = 2 # BWY
elif 'red' in color_table:
color_scheme = 1 # BWR
else:
color_scheme = 0 # BW
else:
# Fallback to protocol detection
color_scheme = capabilities.color_scheme
if not tag_types_manager:
_LOGGER.info(
"Tag types not available, using protocol-detected color_scheme: %d",
color_scheme
)
else:
_LOGGER.warning(
"hw_type %s not in TagTypes, using protocol color_scheme: %d",
hw_type, color_scheme
)

# Build device metadata from capabilities
_LOGGER.warning("OpenDisplay config unavailable, using tagtypes fallback: %s", model_name)
# Store individual fields as fallback
device_metadata = {
"hw_type": hw_type,
"fw_version": self._discovered_device["fw_version"],
"width": capabilities.width,
"height": capabilities.height,
"rotatebuffer": capabilities.rotatebuffer,
"color_scheme": color_scheme,
"color_scheme": capabilities.color_scheme,
"model_name": model_name,
}

Expand All @@ -432,7 +358,7 @@ async def async_step_bluetooth_confirm(
description_placeholders=self._bluetooth_description_placeholders(),
)

except (BLEConnectionError, BLEProtocolError) as e:
except (opendisplay.exceptions.BLEConnectionError, opendisplay.exceptions.ProtocolError) as e:
_LOGGER.error("Error during device interrogation: %s", e)
return self.async_show_form(
step_id="bluetooth_confirm",
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 @@ -6,7 +6,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
from .ble import BLEDeviceMetadata
from .metadata import BLEDeviceMetadata

from .const import DOMAIN, OPENDISPLAY_CONFIG_URL, ATC_CONFIG_URL
from .tag_types import get_hw_string, get_hw_dimensions
Expand Down
2 changes: 1 addition & 1 deletion custom_components/opendisplay/imagegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ async def get_ble_tag_info(self, hass: HomeAssistant, entity_id: str) -> tuple[i
translation_placeholders={"entity_id": entity_id}
)
# Wrap metadata for clean access
from ..ble import BLEDeviceMetadata
from ..metadata import BLEDeviceMetadata
metadata = BLEDeviceMetadata(device_metadata)

# Extract device capabilities
Expand Down
4 changes: 1 addition & 3 deletions custom_components/opendisplay/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"domain": "opendisplay",
"name": "OpenDisplay",
"bluetooth": [
{
"manufacturer_id": 4919
},
{
"manufacturer_id": 9286
}
Expand Down Expand Up @@ -34,6 +31,7 @@
}
},
"requirements": [
"py-opendisplay==7.3.0",
"qrcode[pil]==7.4.2",
"requests_toolbelt==1.0.0",
"websocket-client==1.7.0",
Expand Down
Loading