diff --git a/HMI/README.md b/HMI/README.md index 6f97ec1b..a095971d 100644 --- a/HMI/README.md +++ b/HMI/README.md @@ -144,7 +144,11 @@ The following message can be used to update the content on the cardEntities Page ### screensaver page -`event,buttonPress2,screensaver,enter` +`event,buttonPress2,screensaver,exit` - Touch Event on Screensaver + +`event,screensaverOpen` - Screensaver has opened + + ### cardEntities Page diff --git a/HMI/nspanel.HMI b/HMI/nspanel.HMI index fa7f39f4..bc99b3c1 100644 Binary files a/HMI/nspanel.HMI and b/HMI/nspanel.HMI differ diff --git a/HMI/nspanel.tft b/HMI/nspanel.tft index 1f8f24b4..b936ab79 100644 Binary files a/HMI/nspanel.tft and b/HMI/nspanel.tft differ diff --git a/apps/nspanel-lovelace-ui/luibackend/config.py b/apps/nspanel-lovelace-ui/luibackend/config.py index 2794e824..e9f61430 100644 --- a/apps/nspanel-lovelace-ui/luibackend/config.py +++ b/apps/nspanel-lovelace-ui/luibackend/config.py @@ -6,13 +6,13 @@ from luibackend.exceptions import LuiBackendConfigError LOGGER = logging.getLogger(__name__) - class PageNode(object): def __init__(self, data, parent=None): self.data = data self.name = None self.childs = [] self.parent = parent + self.pos = None if "items" in data: childs = data.pop("items") @@ -21,19 +21,34 @@ class PageNode(object): name = self.data.get("heading", "unkown") if type(self.data) is dict else self.data ptype = self.data.get("type", "unkown") if type(self.data) is dict else "leaf" - #parent = self.parent.data.get("heading", self.parent.data.get("type", "unknown")) if self.parent is not None else "root" self.name = f"{ptype}.{name}" if type(self.data) is dict else self.data def add_child(self, obj): + obj.pos = len(self.childs) self.childs.append(obj) + def next(self): + if self.parent is not None: + pos = self.pos + length = len(self.parent.childs) + return self.parent.childs[(pos+1)%length] + else: + return self + def prev(self): + if self.parent is not None: + pos = self.pos + length = len(self.parent.childs) + return self.parent.childs[(pos-1)%length] + else: + return self + def dump(self, indent=0): """dump tree to string""" tab = ' '*(indent-1) + ' |- ' if indent > 0 else '' name = self.name parent = self.parent.name if self.parent is not None else "root" - dumpstring = f"{tab}{name} -> {parent} \n" + dumpstring = f"{tab}{self.pos}:{name} -> {parent} \n" for obj in self.childs: dumpstring += obj.dump(indent + 1) return dumpstring @@ -61,20 +76,17 @@ class LuiBackendConfig(object): 'timeFormat': "%H:%M", 'dateFormatBabel': "full", 'dateFormat': "%A, %d. %B %Y", + 'weather': 'weather.example', 'pages': [{ - 'type': 'screensaver', - 'weather': 'weather.example', - 'items': [{ - 'type': 'cardEntities', - 'heading': 'Test Entities 1', - 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item'] - }, { - 'type': 'cardGrid', - 'heading': 'Test Grid 1', - 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item'] - } - ], - }], + 'type': 'cardEntities', + 'heading': 'Test Entities 1', + 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item'] + }, { + 'type': 'cardGrid', + 'heading': 'Test Grid 1', + 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item'] + } + ] } def __init__(self, args=None, check=True): @@ -93,9 +105,9 @@ class LuiBackendConfig(object): self._config[k] = v LOGGER.info(f"Loaded config: {self._config}") - root_page = self.get("pages")[0] + root_page = {"items": self.get("pages")} self._page_config = PageNode(root_page) - + LOGGER.info(f"Parsed Page config to the following Tree: \n {self._page_config.dump()}") def check(self): diff --git a/apps/nspanel-lovelace-ui/luibackend/controller.py b/apps/nspanel-lovelace-ui/luibackend/controller.py index f6009fd3..f55fdd8f 100644 --- a/apps/nspanel-lovelace-ui/luibackend/controller.py +++ b/apps/nspanel-lovelace-ui/luibackend/controller.py @@ -1,7 +1,7 @@ import logging import datetime -from pages import LuiPages +from pages import LuiPagesGen LOGGER = logging.getLogger(__name__) @@ -13,38 +13,112 @@ class LuiController(object): self._send_mqtt_msg = send_mqtt_msg self._current_page = None - self._previous_page = None - self._pages = LuiPages(ha_api, config, send_mqtt_msg) + self._pages_gen = LuiPagesGen(ha_api, config, send_mqtt_msg) # Setup time update callback time = datetime.time(0, 0, 0) - ha_api.run_minutely(self._pages.update_time, time) + ha_api.run_minutely(self._pages_gen.update_time, time) # send panel back to startup page on restart of this script - self._pages.page_type("pageStartup") + self._pages_gen.page_type("pageStartup") - #{'type': 'sceensaver', 'weather': 'weather.k3ll3r', 'items': [{'type': 'cardEntities', 'heading': 'Test Entities 1', 'items': ['switch.test_item', {'type': 'cardEntities', 'heading': 'Test Entities 1', 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item', 'switch.test_item']}, 'switch.test_item', 'switch.test_item']}, {'type': 'cardGrid', 'heading': 'Test Grid 1', 'items': ['switch.test_item', 'switch.test_item', 'switch.test_item']}]} - def startup(self, display_firmware_version): LOGGER.info(f"Startup Event; Display Firmware Version is {display_firmware_version}") # send time and date on startup - self._pages.update_time("") - self._pages.update_date("") + self._pages_gen.update_time("") + self._pages_gen.update_date("") - # send panel to root page - self._current_page = self._config.get_root_page() - self._pages.render_page(self._current_page) + # send panel to screensaver + self._pages_gen.page_type("screensaver") + self.screensaver_open() + def screensaver_open(self): + we_name = self._config.get("weather") + self._pages_gen.update_screensaver_weather(kwargs={"weather": we_name, "unit": "°C"}) - def next(self): - return + def detail_open(self, detail_type, entity_id): + if detail_type == "popupShutter": + self._pages_gen.generate_shutter_detail_page(entity_id) + if detail_type == "popupLight": + self._pages_gen.generate_light_detail_page(entity_id) - def button_press(self, entity_id, btype, value): - LOGGER.debug(f"Button Press Event; entity_id: {entity_id}; btype: {btype}; value: {value} ") - if(entity_id == "screensaver" and btype == "enter"): - if self._previous_page is None: - self._pages.render_page(self._current_page.childs[0]) + def button_press(self, entity_id, button_type, value): + LOGGER.debug(f"Button Press Event; entity_id: {entity_id}; button_type: {button_type}; value: {value} ") + # internal buttons + if(entity_id == "screensaver" and button_type == "enter"): + # go to first child of root page (default, after startup) + self._current_page = self._config._page_config.childs[0] + self._pages_gen.render_page(self._current_page) + + if(button_type == "bNext"): + self._current_page = self._current_page.next() + self._pages_gen.render_page(self._current_page) + if(button_type == "bPrev"): + self._current_page = self._current_page.prev() + self._pages_gen.render_page(self._current_page) + if(button_type == "bExit"): + self._pages_gen.render_page(self._current_page) + + # buttons with actions on HA + if button_type == "OnOff": + if value == "1": + self._ha_api.turn_on(entity_id) else: - self._pages.render_page(self._previous_page) + self._ha_api.turn_off(entity_id) + # for shutter / covers + if button_type == "up": + self._ha_api.get_entity(entity_id).call_service("open_cover") + if button_type == "stop": + self._ha_api.get_entity(entity_id).call_service("stop_cover") + if button_type == "down": + self._ha_api.get_entity(entity_id).call_service("close_cover") + if button_type == "positionSlider": + pos = int(value) + self._ha_api.get_entity(entity_id).call_service("set_cover_position", position=pos) + + if button_type == "button": + if entity_id.startswith('scene'): + self._ha_api.get_entity(entity_id).call_service("turn_on") + elif entity_id.startswith('light') or entity_id.startswith('switch') or entity_id.startswith('input_boolean'): + self._ha_api.get_entity(entity_id).call_service("toggle") + else: + self._ha_api.get_entity(entity_id).call_service("press") + + # for media page + if button_type == "media-next": + self._ha_api.get_entity(entity_id).call_service("media_next_track") + if button_type == "media-back": + self._ha_api.get_entity(entity_id).call_service("media_previous_track") + if button_type == "media-pause": + self._ha_api.get_entity(entity_id).call_service("media_play_pause") + if button_type == "hvac_action": + self._ha_api.get_entity(entity_id).call_service("set_hvac_mode", hvac_mode=value) + if button_type == "volumeSlider": + pos = int(value) + # HA wants this value between 0 and 1 as float + pos = pos/100 + self._ha_api.get_entity(entity_id).call_service("volume_set", volume_level=pos) + + # for light detail page + if button_type == "brightnessSlider": + # scale 0-100 to ha brightness range + brightness = int(scale(int(value),(0,100),(0,255))) + self._ha_api.get_entity(entity_id).call_service("turn_on", brightness=brightness) + if button_type == "colorTempSlider": + entity = self._ha_api.get_entity(entity_id) + #scale 0-100 from slider to color range of lamp + color_val = scale(int(value), (0, 100), (entity.attributes.min_mireds, entity.attributes.max_mireds)) + self._ha_api.get_entity(entity_id).call_service("turn_on", color_temp=color_val) + if button_type == "colorWheel": + self._ha_api.log(value) + value = value.split('|') + color = pos_to_color(int(value[0]), int(value[1])) + self._ha_api.log(color) + self._ha_api.get_entity(entity_id).call_service("turn_on", rgb_color=color) + + # for climate page + if button_type == "tempUpd": + temp = int(value)/10 + self._ha_api.get_entity(entity_id).call_service("set_temperature", temperature=temp) \ No newline at end of file diff --git a/apps/nspanel-lovelace-ui/luibackend/mqttListener.py b/apps/nspanel-lovelace-ui/luibackend/mqttListener.py index c1c067f8..ad4a7133 100644 --- a/apps/nspanel-lovelace-ui/luibackend/mqttListener.py +++ b/apps/nspanel-lovelace-ui/luibackend/mqttListener.py @@ -28,11 +28,13 @@ class LuiMqttListener(object): if msg[1] == "startup": display_firmware_version = int(msg[2]) self._controller.startup(display_firmware_version) - if msg[1] == "pageOpen": - self._controller.next() + if msg[1] == "screensaverOpen": + self._controller.screensaver_open() if msg[1] == "buttonPress2": entity_id = msg[2] btype = msg[3] value = msg[4] if len(msg) > 4 else None self._controller.button_press(entity_id, btype, value) + if msg[1] == "pageOpenDetail": + self._controller.detail_open(msg[2], msg[3]) diff --git a/apps/nspanel-lovelace-ui/luibackend/pages.py b/apps/nspanel-lovelace-ui/luibackend/pages.py index e81884e6..d75f26b2 100644 --- a/apps/nspanel-lovelace-ui/luibackend/pages.py +++ b/apps/nspanel-lovelace-ui/luibackend/pages.py @@ -13,7 +13,7 @@ if babel_spec is not None: LOGGER = logging.getLogger(__name__) -class LuiPages(object): +class LuiPagesGen(object): def __init__(self, ha_api, config, send_mqtt_msg): self._ha_api = ha_api @@ -86,13 +86,14 @@ class LuiPages(object): else: up1 = up1.strftime("%a") up2 = up2.strftime("%a") - self._send_mqtt_msg(f"weatherUpdate,?{icon_cur}?{text_cur}?{icon_cur_detail}?{text_cur_detail}?{up1}?{icon1}?{down1}?{up2}?{icon2}?{down2}") def generate_entities_item(self, item): icon = None + name = None if type(item) is dict: - icon = next(iter(item.items()))[1]['icon'] + icon = next(iter(item.items()))[1].get('icon') + name = next(iter(item.items()))[1].get('name') item = next(iter(item.items()))[0] # type of the item is the string before the "." in the item name item_type = item.split(".")[0] @@ -107,7 +108,7 @@ class LuiPages(object): return f",text,{item},{get_icon_id('alert-circle-outline')},17299,Not found check, apps.yaml" # HA Entities entity = self._ha_api.get_entity(item) - name = entity.attributes.friendly_name + name = name if name is not None else entity.attributes.friendly_name if item_type == "cover": icon_id = get_icon_id_ha("cover", state=entity.state, overwrite=icon) return f",shutter,{item},{icon_id},17299,{name}," @@ -122,11 +123,12 @@ class LuiPages(object): icon_id = get_icon_id_ha(item_type, state=entity.state, overwrite=icon) return f",switch,{item},{icon_id},{icon_color},{name},{switch_val}" if item_type in ["sensor", "binary_sensor"]: - device_class = self.get_safe_ha_attribute(entity.attributes, "device_class", "") + device_class = self.entity.attributes.get("device_class", "") icon_id = get_icon_id_ha("sensor", state=entity.state, device_class=device_class, overwrite=icon) - unit_of_measurement = self.get_safe_ha_attribute(entity.attributes, "unit_of_measurement", "") + unit_of_measurement = self.entity.attributes.get("unit_of_measurement", "") value = entity.state + " " + unit_of_measurement - return f",text,{item},{icon_id},17299,{name},{value}" + icon_color = self.getEntityColor(entity) + return f",text,{item},{icon_id},{icon_color},{name},{value}" if item_type in ["button", "input_button"]: icon_id = get_icon_id_ha("button", overwrite=icon) return f",button,{item},{icon_id},17299,{name},PRESS" @@ -145,21 +147,125 @@ class LuiPages(object): self._send_mqtt_msg(command) + def generate_thermo_page(self, item): + if not self._ha_api.entity_exists(item): + command = f"entityUpd,{item},Not found,220,220,Not found,150,300,5" + else: + entity = self._ha_api.get_entity(item) + heading = entity.attributes.friendly_name + current_temp = int(entity.attributes.get("current_temperature", 0)*10) + dest_temp = int(entity.attributes.get("temperature", 0)*10) + status = entity.attributes.get("hvac_action", "") + min_temp = int(entity.attributes.get("min_temp", 0)*10) + max_temp = int(entity.attributes.get("max_temp", 0)*10) + step_temp = int(entity.attributes.get("target_temp_step", 0.5)*10) + icon_res = "" + hvac_modes = entity.attributes.get("hvac_modes", []) + for mode in hvac_modes: + icon_id = get_icon_id('alert-circle-outline') + color_on = 64512 + if mode == "auto": + icon_id = get_icon_id("calendar-sync") + color_on = 1024 + if mode == "heat": + icon_id = get_icon_id("fire") + color_on = 64512 + if mode == "off": + icon_id = get_icon_id("power") + color_on = 35921 + if mode == "cool": + icon_id = get_icon_id("snowflake") + color_on = 11487 + if mode == "dry": + icon_id = get_icon_id("water-percent") + color_on = 60897 + if mode == "fan_only": + icon_id = get_icon_id("fan") + color_on = 35921 + state = 0 + if(mode == entity.state): + state = 1 + icon_res += f",{icon_id},{color_on},{state},{mode}" + + len_hvac_modes = len(hvac_modes) + if len_hvac_modes%2 == 0: + # even + padding_len = int((4-len_hvac_modes)/2) + icon_res = ","*4*padding_len + icon_res + ","*4*padding_len + # use last 4 icons + icon_res = ","*4*5 + icon_res + else: + # uneven + padding_len = int((5-len_hvac_modes)/2) + icon_res = ","*4*padding_len + icon_res + ","*4*padding_len + # use first 5 icons + icon_res = icon_res + ","*4*4 + command = f"entityUpd,{item},{heading},{current_temp},{dest_temp},{status},{min_temp},{max_temp},{step_temp}{icon_res}" + self._send_mqtt_msg(command) + + def generate_media_page(self, item): + if not self._ha_api.entity_exists(item): + command = f"entityUpd,|{item}|Not found|{get_icon_id('alert-circle-outline')}|Please check your|apps.yaml in AppDaemon|50|{get_icon_id('alert-circle-outline')}" + else: + entity = self._ha_api.get_entity(item) + heading = entity.attributes.friendly_name + icon = 0 + title = entity.attributes.get("media_title", "") + author = entity.attributes.get("media_artist", "") + volume = int(entity.attributes.get("volume_level", 0)*100) + iconplaypause = get_icon_id("pause") if entity.state == "playing" else get_icon_id("play") + if "media_content_type" in entity.attributes: + if entity.attributes.media_content_type == "music": + icon = get_icon_id("music") + command = f"entityUpd,|{item}|{heading}|{icon}|{title}|{author}|{volume}|{iconplaypause}" + self._send_mqtt_msg(command) def render_page(self, page): - config = page.data - ptype = config["type"] - LOGGER.info(f"Started rendering of page x with type {ptype}") + LOGGER.info(page) + config = page.data + page_type = config["type"] + LOGGER.info(f"Started rendering of page x with type {page_type}") # Switch to page - self.page_type(ptype) - if ptype == "screensaver": - we_name = config["weather"] - # update weather information - self.update_screensaver_weather(kwargs={"weather": we_name, "unit": "°C"}) - return - if ptype == "cardEntities": + self.page_type(page_type) + if page_type in ["cardEntities", "cardGrid"]: heading = config.get("heading", "unknown") self.generate_entities_page(heading, page.get_items()) return + if page_type == "cardThermo": + LOGGER.info(page.data) + self.generate_thermo_page(page.data.get("item")) + if page_type == "cardMedia": + LOGGER.info(page.data) + self.generate_media_page(page.data.get("item")) - + def generate_light_detail_page(self, entity): + entity = self._ha_api.get_entity(entity) + switch_val = 1 if entity.state == "on" else 0 + icon_color = self.getEntityColor(entity) + brightness = "disable" + color_temp = "disable" + color = "disable" + # scale 0-255 brightness from ha to 0-100 + if entity.state == "on": + if "brightness" in entity.attributes: + brightness = int(scale(entity.attributes.brightness,(0,255),(0,100))) + else: + brightness = "disable" + if "color_temp" in entity.attributes.supported_color_modes: + if "color_temp" in entity.attributes: + # scale ha color temp range to 0-100 + color_temp = int(scale(entity.attributes.color_temp,(entity.attributes.min_mireds, entity.attributes.max_mireds),(0,100))) + else: + color_temp = "unknown" + else: + color_temp = "disable" + list_color_modes = ["xy", "rgb", "rgbw", "hs"] + if any(item in list_color_modes for item in entity.attributes.supported_color_modes): + color = "enable" + else: + color = "disable" + self._send_mqtt_msg(f"entityUpdateDetail,{get_icon_id('lightbulb')},{icon_color},{switch_val},{brightness},{color_temp},{color}") + + def generate_shutter_detail_page(self, entity): + pos = 100-int(entity.attributes.get("current_position", 50)) + self.send_mqtt_msg(f"entityUpdateDetail,{pos}")