From c65c920b91d70c27ff56a7831eb67cdb478ee800 Mon Sep 17 00:00:00 2001 From: LRvdLinden <77990847+LRvdLinden@users.noreply.github.com> Date: Fri, 14 May 2021 14:16:42 +0200 Subject: [PATCH] Add files via upload --- .../custom_components/huesyncbox/__init__.py | 183 +++++++++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 6159 bytes .../__pycache__/config_flow.cpython-38.pyc | Bin 0 -> 3766 bytes .../__pycache__/const.cpython-38.pyc | Bin 0 -> 1367 bytes .../__pycache__/device_action.cpython-38.pyc | Bin 0 -> 3871 bytes .../__pycache__/errors.cpython-38.pyc | Bin 0 -> 808 bytes .../__pycache__/huesyncbox.cpython-38.pyc | Bin 0 -> 4824 bytes .../__pycache__/media_player.cpython-38.pyc | Bin 0 -> 13543 bytes .../huesyncbox/config_flow.py | 117 ++++++ config/custom_components/huesyncbox/const.py | 51 +++ .../huesyncbox/device_action.py | 111 ++++++ config/custom_components/huesyncbox/errors.py | 14 + .../huesyncbox/huesyncbox.py | 138 +++++++ .../huesyncbox/manifest.json | 13 + .../huesyncbox/media_player.py | 354 ++++++++++++++++++ .../huesyncbox/services.yaml | 93 +++++ .../custom_components/huesyncbox/strings.json | 73 ++++ .../huesyncbox/translations/en.json | 73 ++++ .../huesyncbox/translations/nb.json | 73 ++++ .../lovelace_gen/__init__.py | 97 +++++ .../lovelace_gen/manifest.json | 9 + 21 files changed, 1399 insertions(+) create mode 100644 config/custom_components/huesyncbox/__init__.py create mode 100644 config/custom_components/huesyncbox/__pycache__/__init__.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/__pycache__/config_flow.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/__pycache__/const.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/__pycache__/device_action.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/__pycache__/errors.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/__pycache__/huesyncbox.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/__pycache__/media_player.cpython-38.pyc create mode 100644 config/custom_components/huesyncbox/config_flow.py create mode 100644 config/custom_components/huesyncbox/const.py create mode 100644 config/custom_components/huesyncbox/device_action.py create mode 100644 config/custom_components/huesyncbox/errors.py create mode 100644 config/custom_components/huesyncbox/huesyncbox.py create mode 100644 config/custom_components/huesyncbox/manifest.json create mode 100644 config/custom_components/huesyncbox/media_player.py create mode 100644 config/custom_components/huesyncbox/services.yaml create mode 100644 config/custom_components/huesyncbox/strings.json create mode 100644 config/custom_components/huesyncbox/translations/en.json create mode 100644 config/custom_components/huesyncbox/translations/nb.json create mode 100644 config/custom_components/lovelace_gen/__init__.py create mode 100644 config/custom_components/lovelace_gen/manifest.json diff --git a/config/custom_components/huesyncbox/__init__.py b/config/custom_components/huesyncbox/__init__.py new file mode 100644 index 0000000..a9fec7c --- /dev/null +++ b/config/custom_components/huesyncbox/__init__.py @@ -0,0 +1,183 @@ +"""The Philips Hue Play HDMI Sync Box integration.""" +import asyncio +import logging +import json +import os + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import (config_validation as cv) +from homeassistant.helpers.config_validation import make_entity_service_schema +from homeassistant.helpers.service import async_extract_entity_ids +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP + +from .huesyncbox import HueSyncBox, async_remove_entry_from_huesyncbox +from .const import DOMAIN, LOGGER, ATTR_SYNC, ATTR_SYNC_TOGGLE, ATTR_MODE, ATTR_MODE_NEXT, ATTR_MODE_PREV, MODES, ATTR_INTENSITY, ATTR_INTENSITY_NEXT, ATTR_INTENSITY_PREV, INTENSITIES, ATTR_INPUT, ATTR_INPUT_NEXT, ATTR_INPUT_PREV, INPUTS, ATTR_ENTERTAINMENT_AREA, SERVICE_SET_SYNC_STATE, SERVICE_SET_BRIGHTNESS, SERVICE_SET_MODE, SERVICE_SET_INTENSITY, SERVICE_SET_ENTERTAINMENT_AREA + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["media_player"] + +HUESYNCBOX_SET_STATE_SCHEMA = make_entity_service_schema( + { + vol.Optional(ATTR_SYNC): cv.boolean, + vol.Optional(ATTR_SYNC_TOGGLE): cv.boolean, + vol.Optional(ATTR_BRIGHTNESS): cv.small_float, + vol.Optional(ATTR_BRIGHTNESS_STEP): vol.All(vol.Coerce(float), vol.Range(min=-1, max=1)), + vol.Optional(ATTR_MODE): vol.In(MODES), + vol.Optional(ATTR_MODE_NEXT): cv.boolean, + vol.Optional(ATTR_MODE_PREV): cv.boolean, + vol.Optional(ATTR_INTENSITY): vol.In(INTENSITIES), + vol.Optional(ATTR_INTENSITY_NEXT): cv.boolean, + vol.Optional(ATTR_INTENSITY_PREV): cv.boolean, + vol.Optional(ATTR_INPUT): vol.In(INPUTS), + vol.Optional(ATTR_INPUT_NEXT): cv.boolean, + vol.Optional(ATTR_INPUT_PREV): cv.boolean, + vol.Optional(ATTR_ENTERTAINMENT_AREA): cv.string, + } +) + +HUESYNCBOX_SET_BRIGHTNESS_SCHEMA = make_entity_service_schema( + {vol.Required(ATTR_BRIGHTNESS): cv.small_float} +) + +HUESYNCBOX_SET_MODE_SCHEMA = make_entity_service_schema( + {vol.Required(ATTR_MODE): vol.In(MODES)} +) + +HUESYNCBOX_SET_INTENSITY_SCHEMA = make_entity_service_schema( + {vol.Required(ATTR_INTENSITY): vol.In(INTENSITIES), vol.Optional(ATTR_MODE): vol.In(MODES)} +) + +HUESYNCBOX_SET_ENTERTAINMENT_AREA_SCHEMA = make_entity_service_schema( + {vol.Required(ATTR_ENTERTAINMENT_AREA): cv.string} +) + +services_registered = False + +async def async_setup(hass: HomeAssistant, config: dict): + """ + Set up the Philips Hue Play HDMI Sync Box integration. + Only supporting zeroconf, so nothing to do here. + """ + hass.data[DOMAIN] = {} + + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up a config entry for Philips Hue Play HDMI Sync Box.""" + + LOGGER.debug("%s async_setup_entry\nentry:\n%s\nhass.data\n%s" % (__name__, str(entry), hass.data[DOMAIN])) + + huesyncbox = HueSyncBox(hass, entry) + hass.data[DOMAIN][entry.data["unique_id"]] = huesyncbox + + if not await huesyncbox.async_setup(): + return False + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + # Register services on first entry + global services_registered + if not services_registered: + await async_register_services(hass) + services_registered = True + + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + + if unload_ok: + huesyncbox = hass.data[DOMAIN].pop(entry.data["unique_id"]) + await huesyncbox.async_reset() + + # Unregister services when last entry is unloaded + if len(hass.data[DOMAIN].items()) == 0: + await async_unregister_services(hass) + global services_registered + services_registered = False + + return unload_ok + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + # Best effort cleanup. User might not even have the device anymore or had it factory reset. + # Note that the entry already has been unloaded. + try: + await async_remove_entry_from_huesyncbox(entry) + except Exception as e: + LOGGER.warning("Unregistering Philips Hue Play HDMI Sync Box failed: %s ", e) + + +async def async_register_services(hass: HomeAssistant): + async def async_set_sync_state(call): + entity_ids = await async_extract_entity_ids(hass, call) + for _, entry in hass.data[DOMAIN].items(): + if entry.entity and entry.entity.entity_id in entity_ids: + await entry.entity.async_set_sync_state(call.data) + + hass.services.async_register( + DOMAIN, SERVICE_SET_SYNC_STATE, async_set_sync_state, schema=HUESYNCBOX_SET_STATE_SCHEMA + ) + + async def async_set_sync_mode(call): + entity_ids = await async_extract_entity_ids(hass, call) + for _, entry in hass.data[DOMAIN].items(): + if entry.entity and entry.entity.entity_id in entity_ids: + await entry.entity.async_set_sync_mode(call.data.get(ATTR_MODE)) + + hass.services.async_register( + DOMAIN, SERVICE_SET_MODE, async_set_sync_mode, schema=HUESYNCBOX_SET_MODE_SCHEMA + ) + + async def async_set_intensity(call): + entity_ids = await async_extract_entity_ids(hass, call) + for _, entry in hass.data[DOMAIN].items(): + if entry.entity and entry.entity.entity_id in entity_ids: + await entry.entity.async_set_intensity(call.data.get(ATTR_INTENSITY), call.data.get(ATTR_MODE, None)) + + hass.services.async_register( + DOMAIN, SERVICE_SET_INTENSITY, async_set_intensity, schema=HUESYNCBOX_SET_INTENSITY_SCHEMA + ) + + async def async_set_brightness(call): + entity_ids = await async_extract_entity_ids(hass, call) + for _, entry in hass.data[DOMAIN].items(): + if entry.entity and entry.entity.entity_id in entity_ids: + await entry.entity.async_set_brightness(call.data.get(ATTR_BRIGHTNESS)) + + hass.services.async_register( + DOMAIN, SERVICE_SET_BRIGHTNESS, async_set_brightness, schema=HUESYNCBOX_SET_BRIGHTNESS_SCHEMA + ) + + async def async_set_entertainment_area(call): + entity_ids = await async_extract_entity_ids(hass, call) + for _, entry in hass.data[DOMAIN].items(): + if entry.entity and entry.entity.entity_id in entity_ids: + await entry.entity.async_select_entertainment_area(call.data.get(ATTR_ENTERTAINMENT_AREA)) + + hass.services.async_register( + DOMAIN, SERVICE_SET_ENTERTAINMENT_AREA, async_set_entertainment_area, schema=HUESYNCBOX_SET_ENTERTAINMENT_AREA_SCHEMA + ) + + +async def async_unregister_services(hass): + hass.services.async_remove(DOMAIN, SERVICE_SET_SYNC_STATE) + hass.services.async_remove(DOMAIN, SERVICE_SET_BRIGHTNESS) + hass.services.async_remove(DOMAIN, SERVICE_SET_MODE) + hass.services.async_remove(DOMAIN, SERVICE_SET_INTENSITY) + hass.services.async_remove(DOMAIN, SERVICE_SET_ENTERTAINMENT_AREA) diff --git a/config/custom_components/huesyncbox/__pycache__/__init__.cpython-38.pyc b/config/custom_components/huesyncbox/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4aa7500d53d4b41fe254aab3d70034d582cae49e GIT binary patch literal 6159 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 CRw4EP literal 0 HcmV?d00001 diff --git a/config/custom_components/huesyncbox/__pycache__/config_flow.cpython-38.pyc b/config/custom_components/huesyncbox/__pycache__/config_flow.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51ba21b1af8109f60e8650821159f80fa36bd4a3 GIT binary patch literal 3766 zcma)9-EZ7j75CTJ9?wiNP1CY1OJ$)g>`<73iV%Vbwxr!AY<3bgEDP33#__$AapRBa zwcRwM^l2;JD)G)DC6DkQ@XUYVhkfCNec_!-tL1mD=PTW^i>-To&pr3^eEiPw2c3?i z;Cb-Y>L31oNm2eu#PVZ;cn2l_BPynN3R9UDtDY*qny1OH?iu*%aU(H3GqF4?u{~R* zu}0iX9M3_!!OXaow7quH@j8;W;+168TTQxNSJHNTDOvN@lFQy@Re7MWCUc%E%;DCf zb>3#J7lyaaw0))5p8R|>ONY^+JB+hqcbExxZxqGRICr;7?(W6GiMw_0{cU&uBn{nP zWl!BGE%>1biYQC_pW!0Ppr==LtC6D@~1tOZ_+wOI$>Hd|q<_;y$Wl3uHv zTGH=A(378T1u2WU8@RR1{$KI;1zAx@5}ZBMrm~5W8>pCy?X<}^x6`PIf;gIR7gAl` zs6Y6t0*x3skB4FfQ)Kn>&h7dzx5F|oAjdFE##st|JN75l)(CPKbcjf|;hDlr0cVX_qiM&sl zbJ6$KlOQdF*oS%ec{v_uqTmd7GX)RwEJdqTn?P^?R!Sey13rv@{wk$5y_Y$%`_Q>p<~(9?NAsIoGdIPUbhTNFGQ7jBWcag-jp$5Ano`dgS(e?Is_4t^ZFSsI_XJY{Y< z2H;?Ll)I!6_ZaXXXBXKKTIi0_C$Mq0SYuEbv=KCKNhLScGkN!e^eD}aQXJZx|DRls>OcZ0&c znODs+jUJbrb~Z*?UQ||DFAVGa28^XLBl*(^!{ydN7X2r}8rX zN{=DQ-@t_Y`>2$Lt+q8o)6}lIrmoPdby2sc#ZWcHR_refeMOu658t8%FZdRLhJ_-1 z>(^+IZ^1SZOJ%KJQ>4EjhN4BjQn$=`%R;~XiTO-}zbL2LIr(053;xn`CfBF1GvOkG z#D*@5@-TbC#Ytb#y()tcrgFw28*@=mlrzRbF{DH`=(Gx19@r^q5@{x3A74wnlx~8Sv!>%=1I+mZI>kz@ z4W%$nRf(!N8#Jv=aT1iMK-D-oJ@=U~vL$bhEW z#eh=KhZ_yu-S_%Zv7}C`93%bU`I6$Lx~2BtfBDo|<2VTUD2o{v zd9PW`bc#f0ff_e_8O;1>xacI5LK;^hT@v4>^>idhHC%L}+@B>t;J!uM-=yk$sHS!% z?oh|uRL%1N84tye(DxonPUf%Z>+opl$UC3D|o)YFrgA!;! zZ3uu;m5Ov^qT#@@zc!`XnW?`v7=_i2Y&}y8^-N<;gidv+T~*G>04az{Ncc0%qBc3S zT7RM5whU68$*qUHC`EcvlQKS0?&uP163P?Q*4|&MODa)1v-IPII;~pW3r)Xf(Bux{ zz}JEFN41mFDSqbM%$Hi9v5g{1Qu6j_)TKD`Dz> z)%N|zNZzI=wtSyuA-b*K-ha5iy*m&(O}MhTI~e$zJ9qc@{hi&-yF32g?#@otrSAKi zzrnbgDe%vw2+BgGVT&JAi>$Xc_Jvu?7Ga|3pjt)A*HGE2p&6>F*@n|_ZrH2#8x1KJjvp6T$EXvS325NX9Tc7SjJCB0DK^GmzNQQ zqSe|3ZlC_01f`4!;`mi61cf)LRT+E5B4Rs>oJqR2OaSC5m-INlUS)68lKut#9>_Zy Pq)*M($cy2-YFGa+hs?|< literal 0 HcmV?d00001 diff --git a/config/custom_components/huesyncbox/__pycache__/const.cpython-38.pyc b/config/custom_components/huesyncbox/__pycache__/const.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9078d223fa6d3925759e182869ec149555aab0dc GIT binary patch literal 1367 zcmZWo%Wm676s2V9MOm^W$M3|UlQcjUv608J4JykMnSezbBxNLbGo^LM6j6y3L@KcQ zHSIF{d`vLguKEjIbvR@#A|}C^bIyH}oMEG(DdOtC*!bX?yRjbz$Nz}I6_ab@1&f6JCcNVn+xG(hb_vah z#WYc=$R9H<`OVz-#&N)+h&K{@k}Qb)cnOk9_i2a>wi7E8)_Lsu!BimX&Y8>A#D&+T zV;p+kgmF~{)RL73>@J2B&gSe3FMaWmg}gLfME;1Yo;zi{wkq1ZhiPPvC*e?z7B}&P zK{ZJ(W){W9VgRV5ag3MzV77=slUtf2!?Ju?>Th_uzs>e{+5R3n4<~%}l2<6ja6~Du zOvHqK;PINr;tPSL-=n-lYhuWXb9<`mW^MihB8U1s|B(}}B^8SYD5YslC&%BkF2bX7rALbPz94$w* zFc64#Ez;$+uH#s;B5&P8>WGFfNM@9In$nm+YLZV5&+#jZT!SV}*TxyS%$ptDkv`qY1vQ^77VOxK|{{v#IqN)G@ literal 0 HcmV?d00001 diff --git a/config/custom_components/huesyncbox/__pycache__/device_action.cpython-38.pyc b/config/custom_components/huesyncbox/__pycache__/device_action.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a19e2bd73cc1c0144e188a02b3da1c6824d21b7e GIT binary patch literal 3871 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 literal 0 HcmV?d00001 diff --git a/config/custom_components/huesyncbox/__pycache__/huesyncbox.cpython-38.pyc b/config/custom_components/huesyncbox/__pycache__/huesyncbox.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e75cbb1f58b8856e71dd007f3d7c63fd9c5134f7 GIT binary patch literal 4824 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 List[dict]: + """List device actions for Philips Hue Play HDMI Sync Box devices.""" + + actions = [] + + # Get all the integrations entities for this device + registry = await entity_registry.async_get_registry(hass) + for entry in entity_registry.async_entries_for_device(registry, device_id): + + # 1 Mediaplayer per device, no additional checks needed for now + for type_ in ACTION_TYPES.keys(): + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: type_, + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service = ACTION_TYPES[config[CONF_TYPE]][SERVICE] + service_data = ACTION_TYPES[config[CONF_TYPE]].get(SERVICE_DATA, {}) + service_data[ATTR_ENTITY_ID] = config[CONF_ENTITY_ID] + service_domain = ACTION_TYPES[config[CONF_TYPE]].get(CONF_DOMAIN, DOMAIN) + + await hass.services.async_call( + service_domain, service, service_data, blocking=True, context=context + ) diff --git a/config/custom_components/huesyncbox/errors.py b/config/custom_components/huesyncbox/errors.py new file mode 100644 index 0000000..49ada2d --- /dev/null +++ b/config/custom_components/huesyncbox/errors.py @@ -0,0 +1,14 @@ +"""Errors for the HueSyncBox component.""" +from homeassistant.exceptions import HomeAssistantError + + +class HueSyncBoxException(HomeAssistantError): + """Base class for HueSyncBox exceptions.""" + + +class CannotConnect(HueSyncBoxException): + """Unable to connect to the syncbox.""" + + +class AuthenticationRequired(HueSyncBoxException): + """Unknown error occurred.""" diff --git a/config/custom_components/huesyncbox/huesyncbox.py b/config/custom_components/huesyncbox/huesyncbox.py new file mode 100644 index 0000000..8214e97 --- /dev/null +++ b/config/custom_components/huesyncbox/huesyncbox.py @@ -0,0 +1,138 @@ +"""Code to handle a Philips Hue Play HDMI Sync Box.""" +import asyncio + +import aiohuesyncbox +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, LOGGER, MANUFACTURER_NAME +from .errors import AuthenticationRequired, CannotConnect + +class HueSyncBox: + """Manages a single Philips Hue Play HDMI Sync Box.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.api = None # aiohuesyncbox instance + self.entity = None # Mediaplayer entity + + def __str__(self): + output = "" + output += f"{self.config_entry}\n" + output += f"{self.api}\n" + output += f"{self.entity}\n" + return output + + async def async_setup(self, tries=0): + """Set up a huesyncbox based on host parameter.""" + hass = self.hass + + initialized = False + try: + self.api = await async_get_aiohuesyncbox_from_entry_data(self.config_entry.data) + with async_timeout.timeout(10): + await self.api.initialize() + await self.async_update_registered_device_info() # Info might have changed while HA was not running + initialized = True + except (aiohuesyncbox.InvalidState, aiohuesyncbox.Unauthorized): + LOGGER.error("Authorization data for Philips Hue Play HDMI Sync Box %s is invalid. Delete and setup the integration again.", self.config_entry.data["unique_id"]) + return False + except (asyncio.TimeoutError, aiohuesyncbox.RequestError): + LOGGER.error("Error connecting to the Philips Hue Play HDMI Sync Box at %s", self.config_entry.data["host"]) + raise ConfigEntryNotReady + except aiohuesyncbox.AiohuesyncboxException: + LOGGER.exception("Unknown Philips Hue Play HDMI Sync Box API error occurred") + raise ConfigEntryNotReady + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unknown error connecting with Philips Hue Play HDMI Sync Box at %s", self.config_entry.data["host"]) + return False + finally: + if not initialized: + await self.api.close() + + huesyncbox = self # Alias for use in async_stop + async def async_stop(self, event=None) -> None: + """Unsubscribe from events.""" + await huesyncbox.async_reset() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) + + return True + + async def async_reset(self): + """ + Reset this huesyncbox to default state. + """ + if self.api is not None: + await self.api.close() + + return True + + async def async_update_registered_device_info(self): + """ + Update device registry with info from the API + """ + if self.api is not None: + device_registry = ( + await self.hass.helpers.device_registry.async_get_registry() + ) + # Get or create also updates existing entries + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={(DOMAIN, self.api.device.unique_id)}, + name=self.api.device.name, + manufacturer=MANUFACTURER_NAME, + model=self.api.device.device_type, + sw_version= self.api.device.firmware_version, + ) + + # Title formatting is actually in the translation file, but don't know how to get it from here. + # Actually it being in the translation is a bit weird anyway since the frontend can be different language + self.hass.config_entries.async_update_entry(self.config_entry, title=f"{self.api.device.name} ({self.api.device.unique_id})") + + return True + + +async def async_register_aiohuesyncbox(hass, api): + try: + with async_timeout.timeout(30): + registration_info = None + while not registration_info: + try: + registration_info = await api.register("Home Assistant", hass.config.location_name) + except aiohuesyncbox.InvalidState: + # This is expected as syncbox will be in invalid state until button is pressed + pass + await asyncio.sleep(1) + return registration_info + except (asyncio.TimeoutError, aiohuesyncbox.Unauthorized): + raise AuthenticationRequired + except aiohuesyncbox.RequestError: + raise CannotConnect + except aiohuesyncbox.AiohuesyncboxException: + LOGGER.exception("Unknown Philips Hue Play HDMI Sync Box error occurred") + raise CannotConnect + +async def async_get_aiohuesyncbox_from_entry_data(entry_data): + """Create a huesyncbox object from entry data.""" + + LOGGER.debug("%s async_get_aiohuesyncbox_from_entry_data\nentry_data:\n%s" % (__name__, str(entry_data))) + + return aiohuesyncbox.HueSyncBox( + entry_data["host"], + entry_data["unique_id"], + access_token=entry_data.get("access_token"), + port=entry_data["port"], + path=entry_data["path"] + ) + +async def async_remove_entry_from_huesyncbox(entry): + with async_timeout.timeout(10): + async with await async_get_aiohuesyncbox_from_entry_data(entry.data) as api: + await api.unregister(entry.data['registration_id']) diff --git a/config/custom_components/huesyncbox/manifest.json b/config/custom_components/huesyncbox/manifest.json new file mode 100644 index 0000000..b18bf6a --- /dev/null +++ b/config/custom_components/huesyncbox/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "huesyncbox", + "name": "Philips Hue Play HDMI Sync Box", + "config_flow": true, + "documentation": "https://github.com/mvdwetering/huesyncbox", + "issue_tracker": "https://github.com/mvdwetering/huesyncbox/issues", + "requirements": ["aiohuesyncbox==0.0.17"], + "dependencies": ["zeroconf"], + "codeowners": [ + "@mvdwetering" + ], + "zeroconf": ["_huesync._tcp.local."] +} diff --git a/config/custom_components/huesyncbox/media_player.py b/config/custom_components/huesyncbox/media_player.py new file mode 100644 index 0000000..b4731c5 --- /dev/null +++ b/config/custom_components/huesyncbox/media_player.py @@ -0,0 +1,354 @@ +import logging + +import asyncio +import async_timeout + +from homeassistant.components.media_player import ( + MediaPlayerEntity, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOUND_MODE, MEDIA_TYPE_MUSIC, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK) +from homeassistant.const import ( + STATE_OFF, STATE_IDLE, STATE_PLAYING +) +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP + +import aiohuesyncbox + +from .const import MANUFACTURER_NAME, DOMAIN, LOGGER, ATTR_SYNC, ATTR_SYNC_TOGGLE, ATTR_MODE, ATTR_MODE_NEXT, ATTR_MODE_PREV, MODES, ATTR_INTENSITY, ATTR_INTENSITY_NEXT, ATTR_INTENSITY_PREV, INTENSITIES, ATTR_INPUT, ATTR_INPUT_NEXT, ATTR_INPUT_PREV, INPUTS, ATTR_ENTERTAINMENT_AREA + +SUPPORT_HUESYNCBOX = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_SELECT_SOUND_MODE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + +MAX_BRIGHTNESS = 200 + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Setup from configuration.yaml, not supported, only through integration.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup from config_entry.""" + LOGGER.debug("%s async_setup_entry\nconfig_entry:\n%s\nhass.data\n%s" % (__name__, config_entry, str(hass.data[DOMAIN]))) + entity = HueSyncBoxMediaPlayerEntity(hass.data[DOMAIN][config_entry.data["unique_id"]]) + async_add_entities([entity], update_before_add=True) + +async def async_unload_entry(hass, config_entry): + # Not sure what to do, entities seem to disappear by themselves + # No other de-initialization seems needed + pass + + +class HueSyncBoxMediaPlayerEntity(MediaPlayerEntity): + """Representation of a HueSyncBox as mediaplayer.""" + + def __init__(self, huesyncbox): + self._huesyncbox = huesyncbox + self._available = False + huesyncbox.entity = self + + @property + def device_info(self): + """Return the device info.""" + # Only return the identifiers so the entry gets linked properly + # Managing deviceinfo is done elsewhere + return { + 'identifiers': { + (DOMAIN, self._huesyncbox.api.device.unique_id) + }, + } + + async def async_update(self): + try: + with async_timeout.timeout(5): + # Since we need to update multiple endpoints just update all in one call + old_device = self._huesyncbox.api.device + await self._huesyncbox.api.update() + if old_device != self._huesyncbox.api.device: + await self._huesyncbox.async_update_registered_device_info() + self._available = True + except (asyncio.TimeoutError, aiohuesyncbox.AiohuesyncboxException): + self._available = False + + @property + def unique_id(self): + """Return the uniqueid of the entity.""" + return self._huesyncbox.api.device.unique_id + + @property + def available(self): + """Return if the device is available or not.""" + return self._available + + @property + def name(self): + """Return the name of the entity.""" + return self._huesyncbox.api.device.name + + @property + def supported_features(self): + """Flag of media commands that are supported.""" + supported_commands = SUPPORT_HUESYNCBOX + return supported_commands + + @property + def state(self): + """Return the state of the entity.""" + state = STATE_PLAYING + device_state = self._huesyncbox.api.execution.mode + if device_state == 'powersave': + state = STATE_OFF + if device_state == 'passthrough': + state = STATE_IDLE + return state + + async def async_turn_off(self): + """Turn off media player.""" + await self._huesyncbox.api.execution.set_state(mode='powersave') + self.async_schedule_update_ha_state(True) + + async def async_turn_on(self): + """Turn the media player on.""" + await self._huesyncbox.api.execution.set_state(mode='passthrough') + self.async_schedule_update_ha_state(True) + + async def async_media_play(self): + """Send play command.""" + try: + await self._huesyncbox.api.execution.set_state(sync_active=True) + except aiohuesyncbox.InvalidState: + # Most likely another application is already syncing to the bridge + # Since there is no way to ask the user what to do just + # stop the active application and try to activate again + for id, info in self._huesyncbox.api.hue.groups.items(): + if info["active"]: + LOGGER.info(f'Deactivating syncing for {info["owner"]}') + await self._huesyncbox.api.hue.set_group_state(id, active=False) + await self._huesyncbox.api.execution.set_state(sync_active=True) + + self.async_schedule_update_ha_state(True) + + async def async_media_pause(self): + """Send pause command.""" + # Syncbox does not really have "pause", but the default mediaplayer + # card does not work when the mediaplayer only supports Stop, + # so lets implement pause for now to work around that + await self.async_media_stop() + + async def async_media_stop(self): + """Send stop command.""" + await self._huesyncbox.api.execution.set_state(sync_active=False) + self.async_schedule_update_ha_state(True) + + @property + def source(self): + """Return the current input source.""" + selected_source = self._huesyncbox.api.execution.hdmi_source + for input in self._huesyncbox.api.hdmi.inputs: + if input.id == selected_source: + return input.name + + @property + def source_list(self): + """List of available input sources.""" + sources = [] + for input in self._huesyncbox.api.hdmi.inputs: + sources.append(input.name) + return sorted(sources) + + async def async_select_source(self, source): + """Select input source.""" + # Source is the user given name, so needs to be mapped back to a valid API value.""" + for input in self._huesyncbox.api.hdmi.inputs: + if input.name == source: + await self._huesyncbox.api.execution.set_state(hdmi_source=input.id) + self.async_schedule_update_ha_state() + break + + async def async_select_entertainment_area(self, area_name): + """Select entertainmentarea.""" + # Area is the user given name, so needs to be mapped back to a valid API value.""" + group = self._get_group_from_area_name(area_name) + if group: + await self._huesyncbox.api.execution.set_state(hue_target=f"groups/{group.id}") + self.async_schedule_update_ha_state() + + def _get_group_from_area_name(self, area_name): + """Get the group object by entertainment area name.""" + for group in self._huesyncbox.api.hue.groups: + if group.name == area_name: + return group + return None + + def _get_entertainment_areas(self): + """List of available entertainment areas.""" + areas = [] + for group in self._huesyncbox.api.hue.groups: + areas.append(group.name) + return sorted(areas) + + def _get_selected_entertainment_area(self): + """Return the name of the active entertainment area.""" + hue_target = self._huesyncbox.api.execution.hue_target # note that this is a string like "groups/123" + selected_area = None + try: + parts = hue_target.split('/') + id = parts[1] + for group in self._huesyncbox.api.hue.groups: + if group.id == id: + selected_area = group.name + break + except KeyError: + LOGGER.warning("Selected entertainment area not available in groups") + return selected_area + + @property + def device_state_attributes(self): + api = self._huesyncbox.api + mode = api.execution.mode + + attributes = { + 'mode': mode, + 'entertainment_area_list': self._get_entertainment_areas(), + 'entertainment_area': self._get_selected_entertainment_area() + } + + if mode != 'powersave': + attributes['brightness'] = self.scale(api.execution.brightness, [0, MAX_BRIGHTNESS], [0, 1]) + if not mode in MODES: + mode = api.execution.last_sync_mode + attributes['intensity'] = getattr(api.execution, mode).intensity + return attributes + + async def async_set_sync_state(self, sync_state): + """Set sync state.""" + + # Special handling for Toggle specific mode as that cannot be done in 1 call on the API + sync_toggle = sync_state.get(ATTR_SYNC_TOGGLE, None) + mode = sync_state.get(ATTR_MODE, None) + if sync_toggle and mode: + if self._huesyncbox.api.execution.mode != mode: + # When not syncing in the desired mode, just turn on the desired mode, no toggle + sync_toggle = None + else: + # Otherwise just toggle, no mode (setting mode would interfere with the toggle) + mode = None + + # Entertainment area + group = self._get_group_from_area_name(sync_state.get(ATTR_ENTERTAINMENT_AREA, None)) + hue_target = f"groups/{group.id}" if group else None + + state = { + "sync_active": sync_state.get(ATTR_SYNC, None), + "sync_toggle": sync_toggle, + # "hdmi_active": , + # "hdmi_active_toggle": None, + "mode": mode, + "mode_cycle": 'next' if ATTR_MODE_NEXT in sync_state else 'previous' if ATTR_MODE_PREV in sync_state else None, + "hdmi_source": sync_state.get(ATTR_INPUT, None), + "hdmi_source_cycle": 'next' if ATTR_INPUT_NEXT in sync_state else 'previous' if ATTR_INPUT_PREV in sync_state else None, + "brightness": int(self.scale(sync_state[ATTR_BRIGHTNESS], [0, 1], [0, MAX_BRIGHTNESS])) if ATTR_BRIGHTNESS in sync_state else None, + "brightness_step": int(self.scale(sync_state[ATTR_BRIGHTNESS_STEP], [-1, 1], [-MAX_BRIGHTNESS, MAX_BRIGHTNESS])) if ATTR_BRIGHTNESS_STEP in sync_state else None, + "intensity": sync_state.get(ATTR_INTENSITY, None), + "intensity_cycle": 'next' if ATTR_INTENSITY_NEXT in sync_state else 'previous' if ATTR_INTENSITY_PREV in sync_state else None, + "hue_target": hue_target, + } + + await self._huesyncbox.api.execution.set_state(**state) + self.async_schedule_update_ha_state(True) + + async def async_set_sync_mode(self, sync_mode): + """Select sync mode.""" + await self._huesyncbox.api.execution.set_state(mode=sync_mode) + self.async_schedule_update_ha_state(True) + + async def async_set_intensity(self, intensity, mode): + """Set intensity for sync mode.""" + if mode == None: + mode = self.get_mode() + + # Intensity is per mode so update accordingly + state = { + mode: {'intensity': intensity} + } + await self._huesyncbox.api.execution.set_state(**state) + self.async_schedule_update_ha_state(True) + + async def async_set_brightness(self, brightness): + """Set brightness""" + api_brightness = self.scale(brightness, [0, 1], [0, MAX_BRIGHTNESS]) + await self._huesyncbox.api.execution.set_state(brightness=api_brightness) + self.async_schedule_update_ha_state(True) + + def get_mode(self): + mode = self._huesyncbox.api.execution.mode + if not self._huesyncbox.api.execution.mode in MODES: + mode = self._huesyncbox.api.execution.last_sync_mode + return mode + + @staticmethod + def scale(input_value, input_range, output_range): + input_min = input_range[0] + input_max = input_range[1] + input_spread = input_max - input_min + + output_min = output_range[0] + output_max = output_range[1] + output_spread = output_max - output_min + + value_scaled = float(input_value - input_min) / float(input_spread) + + return output_min + (value_scaled * output_spread) + + + # Below properties and methods are temporary to get a "free" UI with the mediaplayer card + + @property + def volume_level(self): + """Volume level of the media player (0..1) is mapped brightness for free UI.""" + return self.scale(self._huesyncbox.api.execution.brightness, [0, MAX_BRIGHTNESS], [0, 1]) + + async def async_set_volume_level(self, volume): + """Set volume level of the media player (0..1), abuse to control brightness for free UI.""" + await self.async_set_brightness(volume) + + @property + def sound_mode(self): + """Return the current sound mode (actually intensity).""" + attributes = self.device_state_attributes + if "intensity" in attributes: + return attributes["intensity"] + return None + + @property + def sound_mode_list(self): + """List of available soundmodes / intensities.""" + return INTENSITIES + + async def async_select_sound_mode(self, sound_mode): + """Select sound mode, abuse for intensity to get free UI.""" + await self.async_set_intensity(sound_mode, None) + + @property + def media_content_type(self): + """Content type of current playing media.""" + # Pretend we are playing music to expose additional data (e.g. mode and intensity) to the player + return MEDIA_TYPE_MUSIC + + @property + def media_title(self): + """Title of current playing media, abuse to disaplay mode + intensity for free UI.""" + return f"{self.get_mode().capitalize()} - {self.sound_mode.capitalize()}" + + @property + def media_artist(self): + """Title of current playing media, abuse to display current source so I have a free UI.""" + return self.source + + async def async_media_previous_track(self): + """Send previous track command, abuse to cycle modes for now.""" + await self._huesyncbox.api.execution.cycle_sync_mode(False) + self.async_schedule_update_ha_state(True) + + async def async_media_next_track(self): + """Send next track command, abuse to cycle modes for now.""" + await self._huesyncbox.api.execution.cycle_sync_mode(True) + self.async_schedule_update_ha_state(True) + diff --git a/config/custom_components/huesyncbox/services.yaml b/config/custom_components/huesyncbox/services.yaml new file mode 100644 index 0000000..f940bb9 --- /dev/null +++ b/config/custom_components/huesyncbox/services.yaml @@ -0,0 +1,93 @@ +# Describes the format for available huesync services + +set_sync_state: + description: Set sync state of the syncbox + fields: + entity_id: + description: Name(s) of entities to set state. + example: 'media_player.huesyncbox' + sync: + description: Set sync state on/off. + example: true + sync_toggle: + description: Toggle sync state. + example: true + brightness: + description: Brightness value to set [0,1]. + example: 0.47 + brightness_step: + description: Change brightness. Should be between -1..1. + example: -0.1 + mode: + description: Mode to set. One of 'video', 'music', 'game' + example: 'video' + mode_next: + description: Select next mode + example: true + mode_prev: + description: Select previous mode + example: true + intensity: + description: Intensity to set. One of 'subtle', 'moderate', 'high', 'intense'. + example: 'moderate' + intensity_next: + description: Select next intensity + example: true + intensity_prev: + description: Select previous intensity + example: true + input: + description: Input to select. One of 'hdmi1', 'hdmi2', 'hdmi3', 'hdmi4'. + example: 'hdmi3' + input_next: + description: Select next input + example: true + input_prev: + description: Select previous input + example: true + entertainment_area: + description: Entertainment area to select. Can be one of the names available in "entertainment_area_list" attribute. + example: 'TV Area' + +set_brightness: + description: Set the brightnes on the sync box + fields: + entity_id: + description: Name(s) of entities to set brightness. + example: 'media_player.huesyncbox' + brightness: + description: Brightness value to set [0,1]. + example: 0.5 + +set_sync_mode: + description: Set sync mode to activate on the sync box + fields: + entity_id: + description: Name(s) of entities to activate sync on. + example: 'media_player.huesyncbox' + mode: + description: Mode to set. One of 'video', 'music', 'game' + example: 'video' + +set_intensity: + description: Set intensity for current mode + fields: + entity_id: + description: Name(s) of entities to set intensity on. + example: 'media_player.huesyncbox' + intensity: + description: Intensity to set. One of 'subtle', 'moderate', 'high', 'intense'. + example: 'moderate' + mode: + description: Optional mode to apply intensity to. If not set intensity is applied to current or last used mode. + example: 'video' + +set_entertainment_area: + description: Select entertainment area + fields: + entity_id: + description: Name(s) of entities to select entertainment area for. + example: 'media_player.huesyncbox' + entertainment_area: + description: Entertainment area to select. Can be one of the names available in "entertainment_area_list" attribute. + example: 'TV Area' diff --git a/config/custom_components/huesyncbox/strings.json b/config/custom_components/huesyncbox/strings.json new file mode 100644 index 0000000..a66b57a --- /dev/null +++ b/config/custom_components/huesyncbox/strings.json @@ -0,0 +1,73 @@ +{ + "config": { + "flow_title": "{name} ({unique_id})", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host" + } + }, + "link": { + "title": "Link Philips Hue Play HDMI Sync Box", + "description": "Press 'Submit' on this dialog to start the linking procedure. Then walk to the Philips Hue Play HDMI Sync Box and hold the button for 3 seconds until it blinks green to register it with Home Assistant." + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "register_failed": "Failed to register, please try again", + "linking": "Unknown linking error occurred." + }, + "abort": { + "no_huesyncboxes": "No Philips Hue Play HDMI Sync Boxes discovered", + "all_configured": "All Philips Hue Play HDMI Sync Boxes are already configured", + "unknown": "Unknown error occurred", + "cannot_connect": "Unable to connect to the device", + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "manual_not_supported": "Manual configuration not supported. Devices should be discovered automatically." + } + }, + "device_automation": { + "action_type": { + "brightness_decrease": "Decrease brightness", + "brightness_increase": "Increase brightness", + "intensity_down": "Decrease intensity", + "intensity_up": "Increase intensity", + "intensity_subtle": "Set intensity subtle", + "intensity_moderate": "Set intensity moderate", + "intensity_high": "Set intensity high", + "intensity_intense": "Set intensity extreme", + "sync_on": "Start light sync", + "sync_off": "Stop light sync", + "sync_toggle": "Toggle light sync", + "sync_video_toggle": "Toggle video mode light sync", + "sync_music_toggle": "Toggle music mode light sync", + "sync_game_toggle": "Toggle game mode light sync", + "sync_video_on": "Start video mode light sync", + "sync_music_on": "Start music mode light sync", + "sync_game_on": "Start game mode light sync", + "input_next": "Select next input", + "input_previous": "Select previous input", + "input_hdmi1": "Set input HDMI1", + "input_hdmi2": "Set input HDMI2", + "input_hdmi3": "Set input HDMI3", + "input_hdmi4": "Set input HDMI4", + "mode_next": "Select next mode", + "mode_previous": "Select previous mode", + "mode_game": "Set mode game", + "mode_music": "Set mode music", + "mode_video": "Set mode video", + "toggle": "Toggle on/off", + "turn_on": "Turn on", + "turn_off": "Turn off" + }, + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + "is_syncing": "{entity_name} is syncing" + } + } +} diff --git a/config/custom_components/huesyncbox/translations/en.json b/config/custom_components/huesyncbox/translations/en.json new file mode 100644 index 0000000..a66b57a --- /dev/null +++ b/config/custom_components/huesyncbox/translations/en.json @@ -0,0 +1,73 @@ +{ + "config": { + "flow_title": "{name} ({unique_id})", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host" + } + }, + "link": { + "title": "Link Philips Hue Play HDMI Sync Box", + "description": "Press 'Submit' on this dialog to start the linking procedure. Then walk to the Philips Hue Play HDMI Sync Box and hold the button for 3 seconds until it blinks green to register it with Home Assistant." + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "register_failed": "Failed to register, please try again", + "linking": "Unknown linking error occurred." + }, + "abort": { + "no_huesyncboxes": "No Philips Hue Play HDMI Sync Boxes discovered", + "all_configured": "All Philips Hue Play HDMI Sync Boxes are already configured", + "unknown": "Unknown error occurred", + "cannot_connect": "Unable to connect to the device", + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "manual_not_supported": "Manual configuration not supported. Devices should be discovered automatically." + } + }, + "device_automation": { + "action_type": { + "brightness_decrease": "Decrease brightness", + "brightness_increase": "Increase brightness", + "intensity_down": "Decrease intensity", + "intensity_up": "Increase intensity", + "intensity_subtle": "Set intensity subtle", + "intensity_moderate": "Set intensity moderate", + "intensity_high": "Set intensity high", + "intensity_intense": "Set intensity extreme", + "sync_on": "Start light sync", + "sync_off": "Stop light sync", + "sync_toggle": "Toggle light sync", + "sync_video_toggle": "Toggle video mode light sync", + "sync_music_toggle": "Toggle music mode light sync", + "sync_game_toggle": "Toggle game mode light sync", + "sync_video_on": "Start video mode light sync", + "sync_music_on": "Start music mode light sync", + "sync_game_on": "Start game mode light sync", + "input_next": "Select next input", + "input_previous": "Select previous input", + "input_hdmi1": "Set input HDMI1", + "input_hdmi2": "Set input HDMI2", + "input_hdmi3": "Set input HDMI3", + "input_hdmi4": "Set input HDMI4", + "mode_next": "Select next mode", + "mode_previous": "Select previous mode", + "mode_game": "Set mode game", + "mode_music": "Set mode music", + "mode_video": "Set mode video", + "toggle": "Toggle on/off", + "turn_on": "Turn on", + "turn_off": "Turn off" + }, + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + "is_syncing": "{entity_name} is syncing" + } + } +} diff --git a/config/custom_components/huesyncbox/translations/nb.json b/config/custom_components/huesyncbox/translations/nb.json new file mode 100644 index 0000000..99c19e3 --- /dev/null +++ b/config/custom_components/huesyncbox/translations/nb.json @@ -0,0 +1,73 @@ +{ + "config": { + "flow_title": "{name} ({unique_id})", + "step": { + "user": { + "title": "Koble til enheten", + "data": { + "host": "Vert" + } + }, + "link": { + "title": "Link Philips Hue Play HDMI Sync Box", + "description": "Trykk på 'Send' i denne dialogboksen for å starte koblingsprosedyren. Gå deretter til Philips Hue Play HDMI Sync Box og hold knappen inne i 3 sekunder til den blinker grønt for å registrere den hos Home Assistant." + } + }, + "error": { + "cannot_connect": "Kunne ikke koble til, prøv igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil", + "register_failed": "Kunne ikke registrere, prøv igjen", + "linking": "Det oppstod en ukjent koblingsfeil." + }, + "abort": { + "no_huesyncboxes": "Ingen Philips Hue Play HDMI-synkroniseringsbokser oppdaget", + "all_configured": "Alle Philips Hue Play HDMI-synkroniseringsbokser er allerede konfigurert", + "unknown": "Det oppstod en ukjent feil", + "cannot_connect": "Kan ikke koble til enheten", + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyt for enheten pågår allerede.", + "manual_not_supported": "Manuell konfigurasjon støttes ikke. Enheter bør oppdages automatisk." + } + }, + "device_automation": { + "action_type": { + "brightness_decrease": "Reduser lysstyrken", + "brightness_increase": "Øk lysstyrken", + "intensity_down": "Reduser intensiteten", + "intensity_up": "Øk intensiteten", + "intensity_subtle": "Angi subtilitet for intensitet", + "intensity_moderate": "Still intensiteten moderat", + "intensity_high": "Sett intensiteten høyt", + "intensity_intense": "Sett intensitet ekstrem", + "sync_on": "Start lys synkronisering", + "sync_off": "Stopp lys synkronisering", + "sync_toggle": "Bytt lys synkronisering", + "sync_video_toggle": "Bytt lysbildesynkronisering for videomodus", + "sync_music_toggle": "Bytt lysmodus for synkronisering av musikkmodus", + "sync_game_toggle": "Veksle spillmodus lys synkronisering", + "sync_video_on": "Start synkronisering av videomodus", + "sync_music_on": "Start synkronisering av musikkmodus", + "sync_game_on": "Start synkronisering av spillmodus", + "input_next": "Velg neste inngang", + "input_previous": "Velg forrige inngang", + "input_hdmi1": "Still inn inngang HDMI1", + "input_hdmi2": "Still inn inngang HDMI2", + "input_hdmi3": "Still inn inngang HDMI3", + "input_hdmi4": "Still inn inngang HDMI4", + "mode_next": "Velg neste modus", + "mode_previous": "Velg forrige modus", + "mode_game": "Sett modus spill", + "mode_music": "Sett modus musikk", + "mode_video": "Sett modusvideo", + "toggle": "Slå på/av", + "turn_on": "Slå på", + "turn_off": "Slå av" + }, + "condition_type": { + "is_on": "{entity_name} er på", + "is_off": "{entity_name} er av", + "is_syncing": "{entity_name} synkroniseres" + } + } +} diff --git a/config/custom_components/lovelace_gen/__init__.py b/config/custom_components/lovelace_gen/__init__.py new file mode 100644 index 0000000..c01fed6 --- /dev/null +++ b/config/custom_components/lovelace_gen/__init__.py @@ -0,0 +1,97 @@ +import os +import logging +import json +import io +import time +from collections import OrderedDict + +import jinja2 + +from homeassistant.util.yaml import loader +from homeassistant.exceptions import HomeAssistantError + +_LOGGER = logging.getLogger(__name__) + +def fromjson(value): + return json.loads(value) + +jinja = jinja2.Environment(loader=jinja2.FileSystemLoader("/")) + +jinja.filters['fromjson'] = fromjson + +llgen_config = {} + +def load_yaml(fname, secrets = None, args={}): + try: + ll_gen = False + with open(fname, encoding="utf-8") as f: + if f.readline().lower().startswith("# lovelace_gen"): + ll_gen = True + + if ll_gen: + stream = io.StringIO(jinja.get_template(fname).render({**args, "_global": llgen_config})) + stream.name = fname + return loader.yaml.load(stream, Loader=lambda _stream: loader.SafeLineLoader(_stream, secrets)) or OrderedDict() + else: + with open(fname, encoding="utf-8") as config_file: + return loader.yaml.load(config_file, Loader=lambda stream: loader.SafeLineLoader(stream, secrets)) or OrderedDict() + except loader.yaml.YAMLError as exc: + _LOGGER.error(str(exc)) + raise HomeAssistantError(exc) + except UnicodeDecodeError as exc: + _LOGGER.error("Unable to read file %s: %s", fname, exc) + raise HomeAssistantError(exc) + + +def _include_yaml(ldr, node): + args = {} + if isinstance(node.value, str): + fn = node.value + else: + fn, args, *_ = ldr.construct_sequence(node) + fname = os.path.abspath(os.path.join(os.path.dirname(ldr.name), fn)) + try: + return loader._add_reference(load_yaml(fname, ldr.secrets, args=args), ldr, node) + except FileNotFoundError as exc: + _LOGGER.error("Unable to include file %s: %s", fname, exc); + raise HomeAssistantError(exc) + +def _uncache_file(ldr, node): + path = node.value + timestamp = str(time.time()) + if '?' in path: + return f"{path}&{timestamp}" + return f"{path}?{timestamp}" + +loader.load_yaml = load_yaml +loader.SafeLineLoader.add_constructor("!include", _include_yaml) +loader.SafeLineLoader.add_constructor("!file", _uncache_file) + +async def async_setup(hass, config): + llgen_config.update(config.get("lovelace_gen")); + return True + +# Allow redefinition of node anchors +import yaml + +def compose_node(self, parent, index): + if self.check_event(yaml.events.AliasEvent): + event = self.get_event() + anchor = event.anchor + if anchor not in self.anchors: + raise yaml.composer.ComposerError(None, None, "found undefined alias %r" + % anchor, event.start_mark) + return self.anchors[anchor] + event = self.peek_event() + anchor = event.anchor + self.descend_resolver(parent, index) + if self.check_event(yaml.events.ScalarEvent): + node = self.compose_scalar_node(anchor) + elif self.check_event(yaml.events.SequenceStartEvent): + node = self.compose_sequence_node(anchor) + elif self.check_event(yaml.events.MappingStartEvent): + node = self.compose_mapping_node(anchor) + self.ascend_resolver() + return node + +yaml.composer.Composer.compose_node = compose_node diff --git a/config/custom_components/lovelace_gen/manifest.json b/config/custom_components/lovelace_gen/manifest.json new file mode 100644 index 0000000..6425022 --- /dev/null +++ b/config/custom_components/lovelace_gen/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "lovelace_gen", + "name": "Lovelace Gen", + "documentation": "", + "dependencies": ["lovelace"], + "codeowners": [], + "requirements": ["jinja2"], + "version": "0.1.1" +}