basic navigation and event handling

This commit is contained in:
Johannes Braun
2022-03-24 22:51:51 +01:00
parent 4810679ac5
commit 0ab7900ec0
7 changed files with 257 additions and 59 deletions

View File

@@ -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

Binary file not shown.

Binary file not shown.

View File

@@ -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):

View File

@@ -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)

View File

@@ -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])

View File

@@ -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}")