Skip to content

Commit 8e78f37

Browse files
committed
Make config work
1 parent f867d4b commit 8e78f37

File tree

5 files changed

+108
-182
lines changed

5 files changed

+108
-182
lines changed

custom_components/dmx/__init__.py

Lines changed: 94 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""ARTNET LED"""
2+
import dataclasses
3+
import json
24
import logging
5+
import os
36
from os import walk
47
from typing import Any
58

@@ -14,13 +17,17 @@
1417
from homeassistant.exceptions import IntegrationError
1518
from homeassistant.helpers import discovery_flow
1619
from homeassistant.helpers.device_registry import DeviceInfo
20+
from homeassistant.helpers.entity import Entity
1721
from homeassistant.helpers.typing import ConfigType
1822

1923
from custom_components.dmx.bridge.artnet_controller import ArtNetController, DiscoveredNode
2024
from custom_components.dmx.client import PortAddress
25+
from custom_components.dmx.client.artnet_server import ArtNetServer
2126
from custom_components.dmx.const import DOMAIN, HASS_DATA_ENTITIES, ARTNET_CONTROLLER, CONF_DATA, UNDO_UPDATE_LISTENER
27+
from custom_components.dmx.fixture.fixture import Fixture
2228
from custom_components.dmx.fixture.parser import parse
2329
from custom_components.dmx.fixture_delegator.delegator import create_entities
30+
from custom_components.dmx.io.dmx_io import Universe
2431
from custom_components.fixtures.ha_fixture import parse_json
2532
from custom_components.fixtures.model import HaFixture
2633

@@ -171,6 +178,12 @@ def port_address_config(value: Any) -> int:
171178
return PortAddress(net, sub_net, universe).port_address
172179

173180

181+
@dataclasses.dataclass
182+
class ManualNode:
183+
host: str
184+
port: int
185+
186+
174187
async def reload_configuration_yaml(event: dict, hass: HomeAssistant):
175188
"""Reload configuration.yaml."""
176189
await hass.services.async_call("homeassistant", "check_config", {})
@@ -219,201 +232,107 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
219232
return True
220233

221234

235+
def process_fixtures(fixture_folder: str) -> dict[str, Fixture]:
236+
fixture_map = {}
237+
238+
if not os.path.isdir(fixture_folder):
239+
log.warning(f"Fixture folder does not exist: {fixture_folder}")
240+
return fixture_map
241+
242+
for filename in os.listdir(fixture_folder):
243+
if not filename.endswith('.json'):
244+
continue
245+
246+
file_path = os.path.join(fixture_folder, filename)
247+
248+
try:
249+
fixture = parse(file_path)
250+
fixture_map[fixture.short_name] = fixture
251+
252+
except json.JSONDecodeError as e:
253+
log.warning("Invalid JSON in file %s: %s", filename, str(e))
254+
except Exception as e:
255+
log.warning("Error processing file %s: %s", filename, str(e))
256+
257+
return fixture_map
258+
259+
222260
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
223261
"""Set up the component."""
224262

225-
# print(f"async_setup_entry: {config_entry}")
226263
hass.data.setdefault(DOMAIN, {})
227264

228-
fixture = parse("fixtures/hydrabeam-300-rgbw.json")
229-
channels = fixture.select_mode("42-channel")
265+
dmx_yaml = entry.data[DOMAIN]
230266

231-
# fixture = parse("fixtures/dj_scan_led.json")
232-
# channels = fixture.select_mode("Normal")
267+
fixtures_yaml = dmx_yaml[CONF_FIXTURES]
268+
fixture_folder = fixtures_yaml[CONF_FOLDER]
233269

234-
# fixture = parse("fixtures/hotbox-rgbw.json")
235-
# channels = fixture.select_mode("9-channel B")
270+
log.debug("Processing fixtures folder: %s/...", fixture_folder)
271+
fixtures = process_fixtures(fixture_folder)
272+
log.debug("Found %d fixtures", len(fixtures))
236273

237-
# fixture = parse("fixtures/jbled-a7.json")
238-
# channels = fixture.select_mode("Standard 16bit")
274+
entities: list[Entity] = []
239275

240-
device = DeviceInfo(
241-
configuration_url=fixture.config_url,
242-
model=fixture.short_name,
243-
identifiers={(DOMAIN, fixture.short_name)}, # TODO use user's name
244-
name=fixture.name
245-
)
276+
# Process ArtNet
277+
if (artnet_yaml := dmx_yaml.get(CONF_NODE_TYPE_ARTNET)) is not None:
278+
279+
max_fps = artnet_yaml[CONF_MAX_FPS]
280+
refresh_every = artnet_yaml[CONF_REFRESH_EVERY]
281+
282+
for universe_dict in artnet_yaml[CONF_UNIVERSES]:
283+
(universe_str, universe_yaml), = universe_dict.items()
284+
port_address = PortAddress.parse(universe_str)
246285

247-
entities = create_entities(100, channels, device)
286+
universe = Universe(port_address)
287+
288+
manual_nodes: list[ManualNode] = []
289+
if (compatibility_yaml := universe_yaml.get(CONF_COMPATIBILITY)) is not None:
290+
send_partial_universe = compatibility_yaml[CONF_SEND_PARTIAL_UNIVERSE]
291+
if (manual_nodes_yaml := compatibility_yaml.get(CONF_MANUAL_NODES)) is not None:
292+
for manual_node_yaml in manual_nodes_yaml:
293+
manual_nodes.append(ManualNode(manual_node_yaml[CONF_HOST], manual_node_yaml[CONF_PORT]))
294+
295+
else:
296+
send_partial_universe = True
297+
298+
devices_yaml = universe_yaml[CONF_DEVICES]
299+
for device_dict in devices_yaml:
300+
(device_name, device_yaml), = device_dict.items()
301+
302+
start_address = device_yaml[CONF_START_ADDRESS]
303+
fixture_name = device_yaml[CONF_FIXTURE]
304+
mode = device_yaml.get(CONF_MODE)
305+
306+
if fixture_name not in fixtures:
307+
log.warning("Could not find fixture '%s'. Ignoring device %s", fixture_name, device_name)
308+
continue
309+
310+
fixture = fixtures[fixture_name]
311+
if not mode:
312+
assert len(fixture.modes) > 0
313+
mode = next(iter(fixture.modes.keys()))
314+
315+
channels = fixture.select_mode(mode)
316+
317+
device = DeviceInfo(
318+
configuration_url=fixture.config_url,
319+
model=fixture.name,
320+
identifiers={(DOMAIN, device_name)},
321+
name=device_name,
322+
)
323+
324+
entities.extend(create_entities(start_address, channels, device, universe))
248325

249-
# log.info(f"The data: {entry.data}")
250-
# entry.data[DOMAIN]['entities'] = entities
251326
hass.data[DOMAIN][entry.entry_id] = {
252327
'entities': entities
253328
}
254329

255-
# fixtures_path = data.get(CONF_FIXTURES, {}).get(CONF_FOLDER, DEFAULT_FIXTURES_FOLDER)
256-
# for (dirpath, dirnames, filenames) in walk(fixtures_path):
257-
# for filename in filenames:
258-
# parser.parse(fixtures_path + "/" + filename)
259-
#
260-
# # This will reload any changes the user made to any YAML configurations.
261-
# # Called during 'quick reload' or hass.reload_config_entry
262-
# hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml)
263-
#
264-
# undo_listener = config_entry.add_update_listener(async_update_options)
265-
# data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
266-
267330
for platform in PLATFORMS:
268331
await hass.config_entries.async_forward_entry_setup(entry, platform)
269332

270333
return True
271334

272335

273-
# async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool:
274-
# """Unload a config entry."""
275-
# unload_ok = await hass.config_entries.async_forward_entry_unload(
276-
# config_entry,
277-
# PLATFORMS,
278-
# )
279-
# data = hass.data[DOMAIN]
280-
# data[config_entry.entry_id][UNDO_UPDATE_LISTENER]()
281-
# if unload_ok:
282-
# data.pop(config_entry.entry_id)
283-
#
284-
# data.pop(DOMAIN)
285-
#
286-
# return unload_ok
287-
288-
289-
#
290-
# print(hass.config_entries.async_entries(DOMAIN))
291-
#
292-
# # for platform in PLATFORMS:
293-
# # hass.async_create_task(
294-
# # )
295-
# # )
296-
#
297-
# hass.async_add_job(hass.config_entries.flow.async_init(
298-
# DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={}
299-
# ))
300-
#
301-
#
302-
#
303-
# return True
304-
305-
#
306-
# platform_config = config.get(DOMAIN)
307-
#
308-
# load_fixtures(hass, platform_config)
309-
#
310-
# entities = []
311-
#
312-
# artnet_config = platform_config.get(CONF_NODE_TYPE_ARTNET)
313-
# if artnet_config:
314-
# max_fps = artnet_config.get(CONF_MAX_FPS)
315-
# refresh_interval = artnet_config.get(CONF_REFRESH_EVERY)
316-
#
317-
# node = ArtNetController(hass, max_fps=max_fps, refresh_every=refresh_interval)
318-
#
319-
# universes_config = artnet_config.get(CONF_UNIVERSES)
320-
# for universe_config in universes_config:
321-
# port_address: PortAddress = next(iter(universe_config.keys()))
322-
# port_config = next(iter(universe_config.values()))
323-
#
324-
# universe = node.add_universe(port_address.universe)
325-
#
326-
# devices_config = port_config.get(CONF_DEVICES)
327-
# for device_config in devices_config:
328-
# device_name: str = next(iter(device_config.keys()))
329-
# fixture_config = next(iter(device_config.values()))
330-
#
331-
# start_address = fixture_config[CONF_START_ADDRESS]
332-
# fixture_name = fixture_config[CONF_FIXTURE]
333-
# mode = fixture_config.get(CONF_MODE)
334-
#
335-
# fixture = get_fixture(fixture_name)
336-
#
337-
# new_entities = implement(fixture, device_name, port_address, universe, start_address, mode)
338-
# entities.extend(new_entities)
339-
#
340-
# hass.data.setdefault(DOMAIN, {})
341-
# hass.data[DOMAIN][HASS_DATA_ENTITIES] = entities
342-
#
343-
# log.info(f"Found {len(entities)} entities")
344-
#
345-
# for platform in PLATFORMS:
346-
# hass.async_create_task(
347-
# hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config)
348-
# )
349-
350-
# hass.helpers.discovery.load_platform('sensor', DOMAIN, {}, config)
351-
#
352-
# return True
353-
#
354-
# {
355-
# vol.Required(CONF_NODE_HOST): cv.string,
356-
# vol.Required(CONF_NODE_UNIVERSES): {
357-
# vol.All(int, vol.Range(min=0, max=1024)): {
358-
# vol.Optional(CONF_SEND_PARTIAL_UNIVERSE, default=True): cv.boolean,
359-
# vol.Optional(CONF_OUTPUT_CORRECTION, default='linear'): vol.Any(
360-
# None, vol.In(AVAILABLE_CORRECTIONS)
361-
# ),
362-
# CONF_DEVICES: vol.All(
363-
# cv.ensure_list,
364-
# [
365-
# {
366-
# vol.Required(CONF_DEVICE_CHANNEL): vol.All(
367-
# vol.Coerce(int), vol.Range(min=1, max=512)
368-
# ),
369-
# vol.Required(CONF_DEVICE_NAME): cv.string,
370-
# vol.Optional(CONF_DEVICE_FRIENDLY_NAME): cv.string,
371-
# vol.Optional(CONF_DEVICE_TYPE, default='dimmer'): vol.In(
372-
# [k.CONF_TYPE for k in __CLASS_LIST]
373-
# ),
374-
# vol.Optional(CONF_DEVICE_TRANSITION, default=0): vol.All(
375-
# vol.Coerce(float), vol.Range(min=0, max=999)
376-
# ),
377-
# vol.Optional(CONF_OUTPUT_CORRECTION, default='linear'): vol.Any(
378-
# None, vol.In(AVAILABLE_CORRECTIONS)
379-
# ),
380-
# vol.Optional(CONF_CHANNEL_SIZE, default='8bit'): vol.Any(
381-
# None, vol.In(CHANNEL_SIZE)
382-
# ),
383-
# vol.Optional(CONF_BYTE_ORDER, default='big'): vol.Any(
384-
# None, vol.In(['little', 'big'])
385-
# ),
386-
# vol.Optional(CONF_DEVICE_MIN_TEMP, default='2700K'): vol.Match(
387-
# "\\d+(k|K)"
388-
# ),
389-
# vol.Optional(CONF_DEVICE_MAX_TEMP, default='6500K'): vol.Match(
390-
# "\\d+(k|K)"
391-
# ),
392-
# vol.Optional(CONF_CHANNEL_SETUP, default=None): vol.Any(
393-
# None, cv.string, cv.ensure_list
394-
# ),
395-
# }
396-
# ],
397-
# )
398-
# },
399-
# },
400-
# vol.Optional(CONF_NODE_PORT, default=6454): cv.port,
401-
# vol.Optional(CONF_NODE_MAX_FPS, default=25): vol.All(
402-
# vol.Coerce(int), vol.Range(min=1, max=50)
403-
# ),
404-
# vol.Optional(CONF_NODE_REFRESH, default=120): vol.All(
405-
# vol.Coerce(int), vol.Range(min=0, max=9999)
406-
# ),
407-
# vol.Optional(CONF_NODE_TYPE, default="artnet-direct"): vol.Any(
408-
# None, vol.In(["artnet-direct", "artnet-controller", "sacn", "kinet"])
409-
# ),
410-
# },
411-
# required = True,
412-
# extra = vol.PREVENT_EXTRA,
413-
414-
415-
#
416-
417336
COMPATIBILITY_SCHEMA = \
418337
vol.Schema(
419338
{
@@ -450,7 +369,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
450369
vol.Optional(CONF_MAX_FPS, default=30): vol.All(vol.Coerce(int), vol.Range(min=0, max=43)),
451370
vol.Optional(CONF_REFRESH_EVERY, default=0.8): cv.positive_float,
452371

453-
vol.Optional(CONF_UNIVERSES): vol.Schema(
372+
vol.Required(CONF_UNIVERSES): vol.Schema(
454373
[{
455374
port_address_config: vol.Schema(
456375
{

custom_components/dmx/entity/light.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class ClaudeLightEntity(LightEntity, RestoreEntity):
3838

3939
def __init__(
4040
self,
41-
matrix_key: str,
41+
matrix_key: str | None,
4242
color_mode: ColorMode,
4343
channels: List[AccumulatedLightChannel],
4444
device: DeviceInfo,
@@ -48,7 +48,10 @@ def __init__(
4848
):
4949
"""Initialize the light entity."""
5050
self._matrix_key = matrix_key
51-
self._attr_name = f"Light {matrix_key}"
51+
if self._matrix_key is None:
52+
self._attr_name = "Light"
53+
else:
54+
self._attr_name = f"Light {matrix_key}"
5255

5356
self._attr_device_info = device
5457
self._attr_unique_id = f"{DOMAIN}_light_{matrix_key}"

custom_components/dmx/fixture_delegator/delegator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ def __build_light_entities(accumulator: dict[str, list[AccumulatedLightChannel]]
179179
def create_entities(
180180
dmx_start: int,
181181
channels: list[None | ChannelOffset | SwitchingChannel],
182-
device: DeviceInfo
182+
device: DeviceInfo,
183+
universe: Universe
183184
) -> list[Entity]:
184-
universe = Universe()
185185
entities = []
186186
lights_accumulator: dict[str, list[AccumulatedLightChannel]] = {}
187187

custom_components/dmx/io/dmx_io.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import asyncio
22
from typing import List, Callable
33

4+
from custom_components.dmx import PortAddress
5+
46

57
class Universe:
6-
def __init__(self):
8+
def __init__(self, port_address: PortAddress):
9+
self.port_address = port_address
10+
711
# Dictionary to store channel value: {channel_number: current_value}
812
self._channel_values = {}
913

staging/platform-configuration3.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dmx:
1111
devices:
1212
- Epic triple lights:
1313
start_address: 11
14-
fixture: hydrabeam-300-rgbw
14+
fixture: CLHB300RGBW
1515
mode: 42-channel
1616

1717
compatibility:
@@ -24,7 +24,7 @@ dmx:
2424
devices:
2525
- DJ led:
2626
start_address: 201
27-
fixture: dj-scan-led
27+
fixture: DJ Scan LED
2828

2929
# triggers: # All triggers will always send HA events, regardless if being configured
3030
# scenes: # Trigger HA scenes FROM external ArtNet controller

0 commit comments

Comments
 (0)