|
1 | 1 | """ARTNET LED""" |
| 2 | +import dataclasses |
| 3 | +import json |
2 | 4 | import logging |
| 5 | +import os |
3 | 6 | from os import walk |
4 | 7 | from typing import Any |
5 | 8 |
|
|
14 | 17 | from homeassistant.exceptions import IntegrationError |
15 | 18 | from homeassistant.helpers import discovery_flow |
16 | 19 | from homeassistant.helpers.device_registry import DeviceInfo |
| 20 | +from homeassistant.helpers.entity import Entity |
17 | 21 | from homeassistant.helpers.typing import ConfigType |
18 | 22 |
|
19 | 23 | from custom_components.dmx.bridge.artnet_controller import ArtNetController, DiscoveredNode |
20 | 24 | from custom_components.dmx.client import PortAddress |
| 25 | +from custom_components.dmx.client.artnet_server import ArtNetServer |
21 | 26 | 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 |
22 | 28 | from custom_components.dmx.fixture.parser import parse |
23 | 29 | from custom_components.dmx.fixture_delegator.delegator import create_entities |
| 30 | +from custom_components.dmx.io.dmx_io import Universe |
24 | 31 | from custom_components.fixtures.ha_fixture import parse_json |
25 | 32 | from custom_components.fixtures.model import HaFixture |
26 | 33 |
|
@@ -171,6 +178,12 @@ def port_address_config(value: Any) -> int: |
171 | 178 | return PortAddress(net, sub_net, universe).port_address |
172 | 179 |
|
173 | 180 |
|
| 181 | +@dataclasses.dataclass |
| 182 | +class ManualNode: |
| 183 | + host: str |
| 184 | + port: int |
| 185 | + |
| 186 | + |
174 | 187 | async def reload_configuration_yaml(event: dict, hass: HomeAssistant): |
175 | 188 | """Reload configuration.yaml.""" |
176 | 189 | await hass.services.async_call("homeassistant", "check_config", {}) |
@@ -219,201 +232,107 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: |
219 | 232 | return True |
220 | 233 |
|
221 | 234 |
|
| 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 | + |
222 | 260 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): |
223 | 261 | """Set up the component.""" |
224 | 262 |
|
225 | | - # print(f"async_setup_entry: {config_entry}") |
226 | 263 | hass.data.setdefault(DOMAIN, {}) |
227 | 264 |
|
228 | | - fixture = parse("fixtures/hydrabeam-300-rgbw.json") |
229 | | - channels = fixture.select_mode("42-channel") |
| 265 | + dmx_yaml = entry.data[DOMAIN] |
230 | 266 |
|
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] |
233 | 269 |
|
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)) |
236 | 273 |
|
237 | | - # fixture = parse("fixtures/jbled-a7.json") |
238 | | - # channels = fixture.select_mode("Standard 16bit") |
| 274 | + entities: list[Entity] = [] |
239 | 275 |
|
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) |
246 | 285 |
|
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)) |
248 | 325 |
|
249 | | - # log.info(f"The data: {entry.data}") |
250 | | - # entry.data[DOMAIN]['entities'] = entities |
251 | 326 | hass.data[DOMAIN][entry.entry_id] = { |
252 | 327 | 'entities': entities |
253 | 328 | } |
254 | 329 |
|
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 | | - |
267 | 330 | for platform in PLATFORMS: |
268 | 331 | await hass.config_entries.async_forward_entry_setup(entry, platform) |
269 | 332 |
|
270 | 333 | return True |
271 | 334 |
|
272 | 335 |
|
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 | | - |
417 | 336 | COMPATIBILITY_SCHEMA = \ |
418 | 337 | vol.Schema( |
419 | 338 | { |
@@ -450,7 +369,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): |
450 | 369 | vol.Optional(CONF_MAX_FPS, default=30): vol.All(vol.Coerce(int), vol.Range(min=0, max=43)), |
451 | 370 | vol.Optional(CONF_REFRESH_EVERY, default=0.8): cv.positive_float, |
452 | 371 |
|
453 | | - vol.Optional(CONF_UNIVERSES): vol.Schema( |
| 372 | + vol.Required(CONF_UNIVERSES): vol.Schema( |
454 | 373 | [{ |
455 | 374 | port_address_config: vol.Schema( |
456 | 375 | { |
|
0 commit comments