mirror of
https://github.com/joBr99/nspanel-lovelace-ui.git
synced 2025-12-25 17:04:25 +01:00
basic navigation and event handling
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
BIN
HMI/nspanel.HMI
BIN
HMI/nspanel.HMI
Binary file not shown.
BIN
HMI/nspanel.tft
BIN
HMI/nspanel.tft
Binary file not shown.
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user