From e3ebffcc724f3383912136482b3ae9554e077cf6 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 6 Apr 2022 21:29:24 +0200 Subject: [PATCH] OpenHASP v1.0 --- .../berry_tasmota/src/embedded/openhasp.be | 764 --------- .../lvgl_examples/lv_tasmota_log_roboto.be | 117 -- tasmota/berry/openhasp/openhasp.tapp | Bin 0 -> 53454 bytes tasmota/berry/openhasp/openhasp_widgets.tapp | Bin 0 -> 8367 bytes tasmota/berry/openhasp/pages.jsonl | 33 + .../openhasp/robotocondensed_latin1.tapp | Bin 0 -> 61585 bytes tasmota/berry/openhasp_src/openhasp.tapp | Bin 0 -> 53454 bytes .../openhasp_src/openhasp_core/autoexec.be | 7 + .../openhasp_src/openhasp_core/openhasp.be | 1489 +++++++++++++++++ .../openhasp_examples}/demo-all.jsonl | 0 .../openhasp_examples}/demo1.jsonl | 1 + .../openhasp_examples}/demo2.jsonl | 0 .../openhasp_examples}/demo3.jsonl | 0 .../openhasp_src/openhasp_examples/lv.jsonl | 44 + .../berry/openhasp_src/openhasp_widgets.tapp | Bin 0 -> 8367 bytes .../openhasp_src/openhasp_widgets/autoexec.be | 6 + .../openhasp_widgets}/lv_tasmota_info.be | 0 .../openhasp_widgets}/lv_tasmota_log.be | 21 +- .../openhasp_widgets}/lv_wifi_graph.be | 0 19 files changed, 1588 insertions(+), 894 deletions(-) delete mode 100644 lib/libesp32/berry_tasmota/src/embedded/openhasp.be delete mode 100644 tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be create mode 100644 tasmota/berry/openhasp/openhasp.tapp create mode 100644 tasmota/berry/openhasp/openhasp_widgets.tapp create mode 100644 tasmota/berry/openhasp/pages.jsonl create mode 100644 tasmota/berry/openhasp/robotocondensed_latin1.tapp create mode 100644 tasmota/berry/openhasp_src/openhasp.tapp create mode 100644 tasmota/berry/openhasp_src/openhasp_core/autoexec.be create mode 100644 tasmota/berry/openhasp_src/openhasp_core/openhasp.be rename {lib/libesp32/berry_tasmota/src/embedded/openhasp => tasmota/berry/openhasp_src/openhasp_examples}/demo-all.jsonl (100%) rename {lib/libesp32/berry_tasmota/src/embedded/openhasp => tasmota/berry/openhasp_src/openhasp_examples}/demo1.jsonl (99%) rename {lib/libesp32/berry_tasmota/src/embedded/openhasp => tasmota/berry/openhasp_src/openhasp_examples}/demo2.jsonl (100%) rename {lib/libesp32/berry_tasmota/src/embedded/openhasp => tasmota/berry/openhasp_src/openhasp_examples}/demo3.jsonl (100%) create mode 100644 tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl create mode 100644 tasmota/berry/openhasp_src/openhasp_widgets.tapp create mode 100644 tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be rename tasmota/berry/{lvgl_examples => openhasp_src/openhasp_widgets}/lv_tasmota_info.be (100%) rename tasmota/berry/{lvgl_examples => openhasp_src/openhasp_widgets}/lv_tasmota_log.be (87%) rename tasmota/berry/{lvgl_examples => openhasp_src/openhasp_widgets}/lv_wifi_graph.be (100%) diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp.be b/lib/libesp32/berry_tasmota/src/embedded/openhasp.be deleted file mode 100644 index 4232a605b..000000000 --- a/lib/libesp32/berry_tasmota/src/embedded/openhasp.be +++ /dev/null @@ -1,764 +0,0 @@ -import string -import json - -# lv.start() -# scr = lv.scr_act() # default screean object -# scr.set_style_bg_color(lv.color(0x0000A0), lv.PART_MAIN | lv.STATE_DEFAULT) - -lv.start() - -hres = lv.get_hor_res() # should be 320 -vres = lv.get_ver_res() # should be 240 - -scr = lv.scr_act() # default screean object -#f20 = lv.montserrat_font(20) # load embedded Montserrat 20 -r20 = lv.font_robotocondensed_latin1(20) -r16 = lv.font_robotocondensed_latin1(16) - -th2 = lv.theme_openhasp_init(0, lv.color(0xFF00FF), lv.color(0x303030), false, r16) -scr.get_disp().set_theme(th2) -# TODO -scr.set_style_bg_color(lv.color(lv.COLOR_WHITE),0) - -# apply theme to layer_top, but keep it transparent -lv.theme_apply(lv.layer_top()) -lv.layer_top().set_style_bg_opa(0,0) - - -# takes an attribute name and responds if it needs color conversion -def is_color_attribute(t) - import string - t = str(t) - # contains `color` but does not contain `color_` - return (string.find(t, "color") >= 0) && (string.find(t, "color_") < 0) -end - -# parse hex string -def parse_hex(s) - import string - s = string.toupper(s) # turn to uppercase - var val = 0 - for i:0..size(s)-1 - var c = s[i] - # var c_int = string.byte(c) - if c == "#" continue end # skip '#' prefix if any - if c == "x" || c == "X" continue end # skip 'x' or 'X' - - if c >= "A" && c <= "F" - val = (val << 4) | string.byte(c) - 55 - elif c >= "0" && c <= "9" - val = (val << 4) | string.byte(c) - 48 - end - end - return val -end - -def parse_color(s) - s = str(s) - if s[0] == '#' - return lv.color(parse_hex(s)) - else - import string - import introspect - var col_name = "COLOR_" + string.toupper(s) - var col_try = introspect.get(lv, col_name) - if col_try != nil - return lv.color(col_try) - end - end - # fail safe with black color - return lv.color(0x000000) -end - -#- ------------------------------------------------------------ - Class `lvh_obj` encapsulating `lv_obj`` - - Provide a mapping for virtual members - Stores the associated page and object id - - Adds specific virtual members used by OpenHASP -- ------------------------------------------------------------ -# -class lvh_obj - # _lv_class refers to the lvgl class encapsulated, and is overriden by subclasses - static _lv_class = lv.obj - static _lv_part2_selector # selector for secondary part (like knob of arc) - - # attributes to ignore when set at object level (they are managed by page) - static _attr_ignore = [ - "id", - "obj", - "page", - "comment", - "parentid", - "auto_size", # TODO not sure it's still needed in LVGL8 - ] - #- mapping from OpenHASP attribute to LVGL attribute -# - #- if mapping is null, we use set_X and get_X from our own class -# - static _attr_map = { - "x": "x", - "y": "y", - "w": "width", - "h": "height", - # arc - "asjustable": nil, - "mode": nil, - "start_angle": "bg_start_angle", - "start_angle1": "start_angle", - "end_angle": "bg_end_angle", - "end_angle1": "end_angle", - "radius": "style_radius", - "border_side": "style_border_side", - "bg_opa": "style_bg_opa", - "border_width": "style_border_width", - "line_width": nil, # depebds on class - "line_width1": nil, # depebds on class - "action": nil, # store the action in self._action - "hidden": nil, # apply to self - "enabled": nil, # apply to self - "click": nil, # synonym to enabled - "toggle": nil, - "bg_color": "style_bg_color", - "bg_grad_color": "style_bg_grad_color", - "type": nil, - # below automatically create a sub-label - "text": nil, # apply to self - "value_str": nil, # synonym to 'text' - "align": nil, - "text_font": nil, - "value_font": nil, # synonym to text_font - "text_color": nil, - "value_color": nil, # synonym to text_color - "value_ofs_x": nil, - "value_ofs_y": nil, - # - "min": nil, - "max": nil, - "val": "value", - "rotation": "rotation", - # img - "src": "src", - "image_recolor": "style_img_recolor", - "image_recolor_opa": "style_img_recolor_opa", - # spinner - "angle": nil, - "speed": nil, - # padding of knob - "pad_top2": nil, - "pad_bottom2": nil, - "pad_left2": nil, - "pad_right2": nil, - "pad_all2": nil, - "radius2": nil, - } - - var _lv_obj # native lvgl object - var _lv_label # sub-label if exists - var _action # action for OpenHASP - - # init - # - create the LVGL encapsulated object - # arg1: parent object - # arg2: json line object - def init(parent, jline) - var obj_class = self._lv_class # need to assign to a var to distinguish from method call - self._lv_obj = obj_class(parent) # instanciate LVGL object - self.post_init() - end - - # post-init, to be overriden - def post_init() - end - - # get LVGL encapsulated object - def get_obj() - return self._lv_obj - end - - def set_action(t) - self._action = str(t) - end - def get_action() - return self._action() - end - - def set_line_width(t) - self._lv_obj.set_style_line_width(int(t), lv.PART_MAIN | lv.STATE_DEFAULT) - end - def get_line_width() - return self._lv_obj.get_style_line_width(lv.PART_MAIN | lv.STATE_DEFAULT) - end - - #- ------------------------------------------------------------ - Mapping of synthetic attributes - - text - - hidden - - enabled - - ------------------------------------------------------------ -# - #- `hidden` attributes mapped to OBJ_FLAG_HIDDEN -# - def set_hidden(h) - if h - self._lv_obj.add_flag(lv.OBJ_FLAG_HIDDEN) - else - self._lv_obj.clear_flag(lv.OBJ_FLAG_HIDDEN) - end - end - - def get_hidden() - return self._lv_obj.has_flag(lv.OBJ_FLAG_HIDDEN) - end - - #- `enabled` attributes mapped to OBJ_FLAG_CLICKABLE -# - def set_enabled(h) - if h - self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) - else - self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) - end - end - - def get_enabled() - return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) - end - # click is synonym to enabled - def set_click(t) self.set_enabled(t) end - def get_click() return self.get_enabled() end - - #- `toggle` attributes mapped to STATE_CHECKED -# - def set_toggle(t) - if t == "TRUE" t = true end - if t == "FALSE" t = false end - if t - self._lv_obj.add_state(lv.STATE_CHECKED) - else - self._lv_obj.clear_state(lv.STATE_CHECKED) - end - end - - def get_toggle() - return self._lv_obj.has_state(lv.STATE_CHECKED) - end - - def set_adjustable(t) - if t - self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) - else - self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) - end - end - def get_adjustable() - return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) - end - - #- set_text: create a `lv_label` sub object to the current object -# - #- (default case, may be overriden by object that directly take text) -# - def check_label() - if self._lv_label == nil - self._lv_label = lv.label(self.get_obj()) - self._lv_label.set_align(lv.ALIGN_CENTER); - end - end - - def set_text(t) - self.check_label() - self._lv_label.set_text(str(t)) - end - def set_value_str(t) self.set_text(t) end - - def get_text() - if self._lv_label == nil return nil end - return self._lv_label.get_text() - end - def get_value_str() return self.get_text() end - - def set_align(t) - var align - self.check_label() - if t == 0 || t == "left" - align = lv.TEXT_ALIGN_LEFT - elif t == 1 || t == "center" - align = lv.TEXT_ALIGN_CENTER - elif t == 2 || t == "right" - align = lv.TEXT_ALIGN_RIGHT - end - self._lv_label.set_style_text_align(align, lv.PART_MAIN | lv.STATE_DEFAULT) - end - - def get_align() - if self._lv_label == nil return nil end - var align self._lv_label.get_style_text_align(lv.PART_MAIN | lv.STATE_DEFAULT) - if align == lv.TEXT_ALIGN_LEFT - return "left" - elif align == lv.TEXT_ALIGN_CENTER - return "center" - elif align == lv.TEXT_ALIGN_RIGHT - return "right" - else - return nil - end - end - - def set_text_font(t) - self.check_label() - var f = lv.font_robotocondensed_latin1(int(t)) - if f != nil - self._lv_label.set_style_text_font(f, lv.PART_MAIN | lv.STATE_DEFAULT) - else - print("HSP: Unsupported font size: robotocondensed-latin1", t) - end - end - def get_text_font() - end - def set_value_font(t) self.set_text_font(t) end - def get_value_font() return self.get_text_font() end - - def set_text_color(t) - self.check_label() - self._lv_label.set_style_text_color(parse_color(t), lv.PART_MAIN | lv.STATE_DEFAULT) - end - def get_text_color() - return self._text_color - end - def set_value_color(t) self.set_text_color(t) end - def get_value_color() return self.get_value_color() end - - def set_value_ofs_x(t) - self.check_label() - self._lv_label.set_x(int(t)) - end - def get_value_ofs_x() - return self._lv_label.get_x() - end - def set_value_ofs_y(t) - self.check_label() - self._lv_label.set_y(int(t)) - end - def get_value_ofs_y() - return self._lv_label.get_y() - end - - # secondary element - def set_pad_top2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_top(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_bottom2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_bottom(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_left2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_left(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_right2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_right(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_all2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_all(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - - def get_pad_top() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_top(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_bottomo() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_bottom(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_left() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_left(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_right() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_right(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_all() - end - - def set_radius2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_radius(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_radius2() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_radius(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - - #- ------------------------------------------------------------ - Mapping of virtual attributes - - ------------------------------------------------------------ -# - def member(k) - # tostring is a special case, we shouldn't raise an exception for it - if k == 'tostring' return nil end - # - if self._attr_map.has(k) - import introspect - var kv = self._attr_map[k] - if kv - var f = introspect.get(self._lv_obj, "get_" + kv) - if type(f) == 'function' - return f(self._lv_obj) - end - else - # call self method - var f = introspect.get(self, "get_" + k) - if type(f) == 'function' - return f(self, k) - end - end - end - raise "value_error", "unknown attribute " + str(k) - end - - def setmember(k, v) - import string - # print(">> setmember", k, v) - # print(">>", classname(self), self._attr_map) - if self._attr_ignore.find(k) != nil - return - elif self._attr_map.has(k) - import introspect - var kv = self._attr_map[k] - if kv - var f = introspect.get(self._lv_obj, "set_" + kv) - # if the attribute contains 'color', convert to lv_color - if type(kv) == 'string' && is_color_attribute(kv) - v = parse_color(v) - end - # print("f=", f, v, kv, self._lv_obj, self) - if type(f) == 'function' - if string.find(kv, "style_") == 0 - # style function need a selector as second parameter - f(self._lv_obj, v, lv.PART_MAIN | lv.STATE_DEFAULT) - else - f(self._lv_obj, v) - end - return - else - print("HSP: Could not find function set_"+kv) - end - else - # call self method - var f = introspect.get(self, "set_" + k) - # print("f==",f) - if type(f) == 'function' - f(self, v) - return - end - end - - else - print("HSP: unknown attribute:", k) - end - # silently ignore if the attribute name is not supported - end -end - -#- ------------------------------------------------------------ - Other widgets -- ------------------------------------------------------------ -# - -#- ------------------------------------------------------------ - label -#- ------------------------------------------------------------# -class lvh_label : lvh_obj - static _lv_class = lv.label - # label do not need a sub-label - def post_init() - self._lv_label = self._lv_obj - end -end - -#- ------------------------------------------------------------ - arc -#- ------------------------------------------------------------# -class lvh_arc : lvh_obj - static _lv_class = lv.arc - static _lv_part2_selector = lv.PART_KNOB - - # line_width converts to arc_width - def set_line_width(t) - self._lv_obj.set_style_arc_width(int(t), lv.PART_MAIN | lv.STATE_DEFAULT) - end - def get_line_width() - return self._lv_obj.get_arc_line_width(lv.PART_MAIN | lv.STATE_DEFAULT) - end - def set_line_width1(t) - self._lv_obj.set_style_arc_width(int(t), lv.PART_INDICATOR | lv.STATE_DEFAULT) - end - def get_line_width1() - return self._lv_obj.get_arc_line_width(lv.PART_INDICATOR | lv.STATE_DEFAULT) - end - - def set_min(t) - self._lv_obj.set_range(int(t), self.get_max()) - end - def set_max(t) - self._lv_obj.set_range(self.get_min(), int(t)) - end - def get_min() - return self._lv_obj.get_min_value() - end - def get_max() - return self._lv_obj.get_max_value() - end - def set_type(t) - var mode - if t == 0 mode = lv.ARC_MODE_NORMAL - elif t == 1 mode = lv.ARC_MODE_REVERSE - elif t == 2 mode = lv.ARC_MODE_SYMMETRICAL - end - if mode != nil - self._lv_obj.set_mode(mode) - end - end - def get_type() - return self._lv_obj.get_mode() - end - # mode - def set_mode(t) - var mode - if mode == "expand" self._lv_obj.set_width(lv.SIZE_CONTENT) - elif mode == "break" mode = lv.LABEL_LONG_WRAP - elif mode == "dots" mode = lv.LABEL_LONG_DOT - elif mode == "scroll" mode = lv.LABEL_LONG_SCROLL - elif mode == "loop" mode = lv.LABEL_LONG_SCROLL_CIRCULAR - elif mode == "crop" mode = lv.LABEL_LONG_CLIP - end - if mode != nil - self._lv_obj.lv_label_set_long_mode(mode) - end - end - def get_mode() - end - -end - -#- ------------------------------------------------------------ - switch -#- ------------------------------------------------------------# -class lvh_switch : lvh_obj - static _lv_class = lv.switch - static _lv_part2_selector = lv.PART_KNOB -end - -#- ------------------------------------------------------------ - spinner -#- ------------------------------------------------------------# -class lvh_spinner : lvh_arc - static _lv_class = lv.spinner - - # init - # - create the LVGL encapsulated object - # arg1: parent object - # arg2: json line object - def init(parent, jline) - var angle = jline.find("angle", 60) - var speed = jline.find("speed", 1000) - self._lv_obj = lv.spinner(parent, speed, angle) - self.post_init() - end - - # ignore attributes, spinner can't be changed once created - def set_angle(t) end - def get_angle() end - def set_speed(t) end - def get_speed() end -end - -#- creat sub-classes of lvh_obj and map the LVGL class in static '_lv_class' attribute -# -class lvh_bar : lvh_obj static _lv_class = lv.bar end -class lvh_btn : lvh_obj static _lv_class = lv.btn end -class lvh_btnmatrix : lvh_obj static _lv_class = lv.btnmatrix end -class lvh_checkbox : lvh_obj static _lv_class = lv.checkbox end -class lvh_dropdown : lvh_obj static _lv_class = lv.dropdown end -class lvh_img : lvh_obj static _lv_class = lv.img end -class lvh_line : lvh_obj static _lv_class = lv.line end -class lvh_roller : lvh_obj static _lv_class = lv.roller end -class lvh_slider : lvh_obj static _lv_class = lv.slider end -class lvh_textarea : lvh_obj static _lv_class = lv.textarea end - -#- ---------------------------------------------------------------------------- - Class `lvh_page` encapsulating `lv_obj` as screen (created with lv.obj(0)) -- ----------------------------------------------------------------------------- -# -# ex of transition: lv.scr_load_anim(scr, lv.SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false) -class lvh_page - var _obj_id # (map) of objects by id numbers - var _page_id # (int) id number of the page - var _lv_scr # (lv_obj) lvgl screen object - - #- init(page_number) -# - def init(page_number) - import global - - # if no parameter, default to page #1 - if page_number == nil page_number = 1 end - - self._page_id = page_number # remember our page_number - self._obj_id = {} # init list of objects - if page_number == 1 - self._lv_scr = lv.scr_act() # default screen - elif page_number == 0 - self._lv_scr = lv.layer_top() # top layer, visible over all screens - else - self._lv_scr = lv.obj(0) # allocate a new screen - # self._lv_scr.set_style_bg_color(lv.color(0x000000), lv.PART_MAIN | lv.STATE_DEFAULT) # set black background - self._lv_scr.set_style_bg_color(lv.color(0xFFFFFF), lv.PART_MAIN | lv.STATE_DEFAULT) # set white background - end - - # create a global for this page of form p, ex p1 - var glob_name = string.format("p%i", self._page_id) - global.(glob_name) = self - end - - #- retrieve lvgl screen object for this page -# - def get_scr() - return self._lv_scr - end - - #- add an object to this page -# - def set_obj(id, o) - self._obj_id[id] = o - end - def get_obj(id) - return self._obj_id.find(id) - end - - #- return id of this page -# - def id() - return self._page_id - end - - #- show this page, with animation -# - def show(anim, duration) - # ignore if there is no screen, like for id 0 - if self._lv_scr == nil return nil end - # ignore if the screen is already active - if self._lv_scr._p == lv.scr_act()._p return end # do nothing - - # default animation is lv.SCR_LOAD_ANIM_MOVE_RIGHT - if anim == nil anim = lv.SCR_LOAD_ANIM_MOVE_RIGHT end - # default duration of 500ms - if duration == nil duration = 500 end - - # load new screen with anumation, no delay, 500ms transition time, no auto-delete - lv.scr_load_anim(self._lv_scr, lv.SCR_LOAD_ANIM_MOVE_RIGHT, duration, 0, false) - end -end - -#- pages -# -var lvh_page_cur = lvh_page(1) -var lvh_pages = { 1: lvh_page_cur } # always create page #1 - -f = open("pages.jsonl","r") -var jsonl = string.split(f.read(), "\n") -f.close() - -#- ------------------------------------------------------------ - Parse page information - - Create a new page object if required - Change the active page -- ------------------------------------------------------------ -# -def parse_page(jline) - if jline.has("page") && type(jline["page"]) == 'int' - var page = int(jline["page"]) - # does the page already exist? - if lvh_pages.has(page) - # yes, just change the current page - lvh_page_cur = lvh_pages[page] - else - # no, create a new page - lvh_page_cur = lvh_page(page) - lvh_pages[page] = lvh_page_cur - end - end -end - -#- ------------------------------------------------------------ - Parse single object - -- ------------------------------------------------------------ -# -def parse_obj(jline, page) - import global - import introspect - - # line must contain 'obj' and 'id', otherwise it is ignored - if jline.has("obj") && jline.has("id") && type(jline["id"]) == 'int' - # 'obj_id' must be between 1 and 254 - var obj_id = int(jline["id"]) - if obj_id < 1 || obj_id > 254 - raise "value error", "invalid id " + str(obj_id) - end - - # extract openhasp class, prefix with `lvh_`. Ex: `btn` becomes `lvh_btn` - var obj_type = jline["obj"] - - # extract parent - var parent - var parent_id = int(jline.find("parentid")) - if parent_id != nil - var parent_obj = lvh_page_cur.get_obj(parent_id) - if parent_obj != nil - parent = parent_obj._lv_obj - end - end - if parent == nil - parent = page.get_scr() - end - - # check if a class with the requested name exists - var obj_class = introspect.get(global, "lvh_" + obj_type) - if obj_class == nil - raise "value error", "cannot find object of type " + str(obj_type) - end - - # instanciate the object, passing the lvgl screen as paren object - var obj = obj_class(parent, jline) - - # add object to page object - lvh_page_cur.set_obj(obj_id, obj) - # set attributes - # try every attribute, if not supported it is silently ignored - for k:jline.keys() - # introspect.set(obj, k, jline[k]) - obj.(k) = jline[k] - end - - # create a global variable for this object of form pb, ex p1b2 - var glob_name = string.format("p%ib%i", lvh_page_cur.id(), obj_id) - global.(glob_name) = obj - end -end - -# ex: -# {'page': 1, 'h': 50, 'obj': 'label', 'hidden': false, 'text': 'Hello', 'x': 5, 'id': 1, 'enabled': true, 'y': 5, 'w': 50} -# {"page":1,"id":2,"obj":"btn","x":5,"y":90,"h":90,"w":50,"text":"World","enabled":false,"hidden":false} - -#- ------------------------------------------------------------ - Parse jsonl file line by line - -- ------------------------------------------------------------ -# -tasmota.yield() -for j:jsonl - var jline = json.load(j) - - # parse page first - if type(jline) == 'instance' - parse_page(jline) - parse_obj(jline, lvh_page_cur) - end -end diff --git a/tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be b/tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be deleted file mode 100644 index 79462aa85..000000000 --- a/tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be +++ /dev/null @@ -1,117 +0,0 @@ -# lv_tasmota_log class - -class lv_tasmota_log_roboto : lv.obj - var label # contains the sub lv_label object - var lines - var line_len - var log_reader - var log_level - - def init(parent) - super(self).init(parent) - self.set_width(parent.get_width()) - self.set_pos(0, 0) - - self.set_style_bg_color(lv.color(0x000000), lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_bg_opa(255, lv.PART_MAIN | lv.STATE_DEFAULT) - self.move_background() - self.set_style_border_opa(255, lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_radius(0, lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_pad_all(2, lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_border_color(lv.color(0x0099EE), lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_border_width(1, lv.PART_MAIN | lv.STATE_DEFAULT) - self.refr_size() - self.refr_pos() - - self.label = lv.label(self) - self.label.set_width(self.get_width() - 12) - - self.label.set_style_text_color(lv.color(0x00FF00), lv.PART_MAIN | lv.STATE_DEFAULT) - self.label.set_long_mode(lv.LABEL_LONG_CLIP) - var roboto12 = lv.font_robotocondensed_latin1(12) - self.label.set_style_text_font(roboto12, lv.PART_MAIN | lv.STATE_DEFAULT) - # var lg_font = lv.font_montserrat(10) - # self.set_style_text_font(lg_font, lv.PART_MAIN | lv.STATE_DEFAULT) - self.label.set_text("") # bug, still displays "Text" - - self.add_event_cb( / obj, evt -> self.size_changed_cb(obj, evt), lv.EVENT_SIZE_CHANGED | lv.EVENT_STYLE_CHANGED | lv.EVENT_DELETE, 0) - - self.lines = [] - self.line_len = 0 - self.log_reader = tasmota_log_reader() - self.log_level = 2 - self._size_changed() - - tasmota.add_driver(self) - end - - def set_lines_count(line_len) - if line_len > self.line_len # increase lines - for i: self.line_len .. line_len-1 - self.lines.insert(0, "") - end - elif line_len < self.line_len # decrease lines - for i: line_len .. self.line_len-1 - self.lines.remove(0) - end - end - self.line_len = line_len - end - - def _size_changed() - # print(">>> lv.EVENT_SIZE_CHANGED") - var pad_hor = self.get_style_pad_left(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_pad_right(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_border_width(lv.PART_MAIN | lv.STATE_DEFAULT) * 2 - + 3 - var pad_ver = self.get_style_pad_top(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_pad_bottom(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_border_width(lv.PART_MAIN | lv.STATE_DEFAULT) * 2 - + 3 - var w = self.get_width() - pad_hor - var h = self.get_height() - pad_ver - self.label.set_size(w, h) - - # compute how many lines should be displayed - var h_font = lv.font_get_line_height(self.label.get_style_text_font(0)) # current font's height - var lines_count = ((h * 2 / h_font) + 1 ) / 2 - # print("h_font",h_font,"h",h,"lines_count",lines_count) - self.set_lines_count(lines_count) - end - - def size_changed_cb(obj, event) - var code = event.code - if code == lv.EVENT_SIZE_CHANGED || code == lv.EVENT_STYLE_CHANGED - self._size_changed() - elif code == lv.EVENT_DELETE - tasmota.remove_driver(self) - end - end - - def every_second() - var dirty = false - for n:0..20 - var line = self.log_reader.get_log(self.log_level) - if line == nil break end # no more logs - self.lines.remove(0) # remove first line - self.lines.push(line) - dirty = true - end - if dirty self.update() end - end - - def update() - var msg = self.lines.concat("\n") - self.label.set_text(msg) - end -end - -return lv_tasmota_log_roboto - -# import lv_tasmota_log -# var lg = lv_tasmota_log(scr, 6) -# lg.set_size(hres, 95) -# lg.set_pos(0, stat_line.get_height() + 40) -# tasmota.add_driver(lg) - -# var roboto12 = lv.font_robotocondensed_latin1(12) lg.set_style_text_font(roboto12, lv.PART_MAIN | lv.STATE_DEFAULT) \ No newline at end of file diff --git a/tasmota/berry/openhasp/openhasp.tapp b/tasmota/berry/openhasp/openhasp.tapp new file mode 100644 index 0000000000000000000000000000000000000000..f664abf69947b70ba600494a7b6b7c4a3c828b5b GIT binary patch literal 53454 zcmdsgOK&7el3vZutbnOGEwB)D5QLbddYRNDv&rh7fkai+fL+Dzu9O(P%7 zAQ{bMMk*tdMfT&M%bfPG|6l<@m%V8<|3KLDLO(*+1$5k_zRw;W9+{DBR!>mEC}+Bu z8R2ejZf0(7Zf5TO^4CB7(cL@zjQ;9(hkyO>!QyZ5o&Wvh&cD1fSj_YEGCk@J)6Win zeCLOq-#;66@!#U|CwG4EqksCrojX7Ja}?c3iuqtRPx5IxIUN+!yV?0PkAL6YNJi;# zID$p3d64J3Bj_E{{8OX{@FkN z!<{?)k9~Bcy^nvFJsbY-|6bF_7wFxm`>$UnNBQ{_s137mHor35?{1)viy}?>OMUN4 zr+{cOKTVTgy?*ihDQS^SGVNS8N&3^lNm_KjEb_^?FM#$*3iJeh%f1pSB>kOaV>=mN zbYJejI_y2$fAT!}hCg2)?jIiX9v^(X|Jl>SWP78ZWJNMdr{lp9$n`dxpQ0+EUJPb| zHpv4+yWR5cx3=!y-DvuA8xWI6<3Uj*{qeUBcAOn81EUa(lHpb2;&K}_Xq-<+LQ^B@-Gt+Gj=uFkOnvo$vIa6F{A@%o zW*pa`TrR{LU!05+tx>Wi9d(3?5X}4{oz1|I2`N-8hEg;wsx3~TOYZfGbPSq;Vl^h|r4L7SXS@?nx6Cxh7$WSLkF=JQ!LT+GSYJjqTbU~=;I6s>bS z4xq4uWZ`CL*<;Olezyyukov_MLpds zvae8HF*tnj_(d|w=c;0}`6dK*oNJ4%#py*nO*f}i-|XiuZec_DCd|vo|M!!vL7sGQ7QhHX)*^t zWFDF(qihCA8-w?gY865xAgOb3xfjNCRILA2G%m3PT>(*A?!V1O^V5E4@X2vdKo|v+ z#dzGov?db<=djPE11IwBc$S|h`2x!N?L>zXp!j$c@k3is;9ui0Wbd-I$A3(GuK4T9 zf4$`|!Ni}O^2uqMot(NXfp)Va1vMzXT%e)hI7O*RHa3`;oU0FHV(kqkCsMFAJn6+x ztXA}BmyJfh9hr18sz51!8;tNkAcS9De>NCpi$Z~4jnkg}w#vhNHcDq06@oq5XOZ|} zwVaYk5RD2}h=-2m(?Qvu{eEXS<7|@lygkyHj9mTDW`fCqxh5|K0UR%@lPqiNud6;d zqTz8}O+ke=9n~erfL5W>fEGUP>Q9xK(`*E7egkN<=w!qZgw{QdV-bB6m6_+$DiA)1-VgKnJU_1% zv1d^!*!0yRd{7nhjpXeto2T2bd9REkSJR4~*+_;d=7ofl+d0h<0J*Xm1O^?fhwbqI zMXWYwsB4CUjjvi^x)_WXDU=vlv35Ky3Df2r4Q^`NgE1@ut6O5zJI*m<$C^~zi5{;G zdOoQLEt?TY5nz{xQgjt^{Ja+QDrBjmpcO`bT=Y1nEW_q=&Mx6OXHsaR^Q_wQ^TA4O z)M^QE7o-{1Zl;7<;k$~@Mv|SMDCA;x#G3qLqLrP)FzI2ku8vaF2oLK@R<(9iT3&d& z1(pYsbf#_lapUS{o zeT*SceW-I@<=I9u!(uua4!96mOh+)d#4HZRu%TZprd*}72fz)>MDR#gt9=R`jTi72 z4`JginFRh2f;l_K%B#{@k=T_g;pz2tQdnZOX49Hj}C!x^U^ zu06OkhQWS=&qmVDCi5+~XzA?Ws!&02wd6~r^{_Tzwa&8(cr>_HQqwpBAS!KrZM?xC zcGZ_;$4Ppb6?5-JPnN9LRu>4mfY1eQ6$P8?uLrRKl0justY{zjQa9;qZAg&|TPG<_ z^Tl{H*_=b2C)DAP%6?==^pheZLtqZS9ZcXR!AcU+e`0F~n8)wyIz2gFOvH96niYvx z94+bO1Xh`^2()*ysK7|zQ_3@mypdq}2yDfc;Lzdhw5_8Ih8h;2vZM_B@R4#YJ#ECo zse~01RyuT#O@*>HCY_otCl1xeQZ4iiTMZr01}9uRDrEv)xn|3h_u{=^Ir@ZOIi<67v0}i5NtP$q=Hs~X`AYhdZ;C{u}?Mt2B z{mX}a`gm@Gc$jVINP}Yo`0L7`7m#Vs*=z!nN8IHzb-K*b`2t#CnCIj6EQOC73-r@= zOK|f+d3@x%Xi*%;rV%>XVPKj+B0@Y*{N(988}iuqWo(6e^iRuh^4@w&BCReD&&+ zPd@rcd@2bZY%Kl0ugs)N=wTnC1NWZ#^5#dUP)-mFxQ4rrUOavAs#h)B2IfhBmX7+s z98(m$_}QdLCk15vLj6nIAdpF_~SOwgH{_u@^llEow-(36_Nd#aV^`%>?dD z1RLNWJkBmV30!WVhnbH%pZg1|WCE_@Vb2Qqu=GK9p2HMH5U1D@3`G3FFL_9ISPhzK zn6glB$DTk$l5B6M+okUl)wg%;g&Jf>Y~oG!MR-WNWtJTE(4DgN;nh5C!(}Y}$3I{u zDn3YB8!dk%f2laK39KHBIuc6gu(`2mG+;eYWfjb2EBWRd1NnJN^v^1o%T1DS^K)HM zR)IVOF8i(IFa82R?&JH%t?-`xBthEzbN_zw;TEDoMpf$@JAFrFgUR1*r`}8WgLG!73xV9gDAWDIEuy zsFN&v1}4vPhG|@Mh4=ZmM->4)TB-|L${zunSjhdwkKMmJ@I; zJtQt3vHVXTaII{B5`^SLtV6G?m{)Yt`QHsuBuOzihFQ!Yp6VQ-h`?Ni=gNoxjocdz zESeqw92HA8FRgqvan)X=PM5$XHH`%|Sw*T1W+%IQ>JSGLX@XkSwgkyA#5EpZ-W+|& z4$vumat})ROZvL{D`8V#c*K~5MaVooNAwemUTuY5ORSeH(iEV7u$QzEVBJ#m3=r6d zjt<6%QjD%_WhJipx0u2#c7frZVWd5-6lL!%@VuxR{F$^L) zVRfbVK#Isy;Eoa;N)S6L3oYgmJSn4U5ye7OuUd%ELHnYBKZ`vah`z*^5!RTP-4V0W%QUK+tkB?;Qy(R9P0jd3zw><)AHX4~h6uKiKq;$K#i3_YISn?%QOZyqBwR=zbbBneXwO;F zSpR+#`&?FCEe&^(aY=RO_%cQNXAUAU-uw-8#%LtUfB|9+u}g$TLcJ6x2Bd)?zs6+h zqFyP;?~_@bL`*(B=xX(nMTWns zO`9>}URVYEX-*3FNr|E4nb1f!av_Ar)hxQ8_J~f9A4z}%3tPTh4tPth5tWTZl-uWn zD~aBVULG-0D^pK0Vx0H^3iXs8FJB$JetqzmeS!rYzcEV5n=$lT{7!21p1yc}D6eNo zzTi42g8S;=>A}7O9_LuYN*ZuvAHkJc6Fy*hY#u&)grox;_QVsr~AwAPStn zDRlvtXzAeSR7bVoMyvW59IqYjVOEPqyVMdu7kfHRJxi|&Q^RfquAi`MhB6DZn2BDXxRV2ibkh)|%9~5W6xXRXp$C`py(L?d0lKJsdXLRz7HNa-< z6B>xw7FUj+8$09I6&^t|R3#+Z1-Uj&U7nBvQ>l zi2_tt*07}}WRyTA5=o>gpyI^SKDnBo@=K9VPic=LKNOk+)|b@L_?su1@)1-ov;0g0 z!;Is@LropENoJhqQ)}4d0MpK+kC zO2b0?z-2nu97ai9*@{0xaEr!8EJKtcu7{ba0Y0%I1Aqx{lwv^!8h6mRU?zbpLrO3Z zwj@XgUQYG(Jm1sqDEAp&IE1A)sIDlR@uegbcs&W$=?LhSlRy`%)=|6U7ch>BR~GSg zz6P=vL?wU4Th^u5axiaie%)ewt-Y^X)AzFBds%C*^2N0?<6QmK~epsD8@)_s@tPogUl9QZ4N61vJoB{ zM>b0NOE%Fh7A%owU3N1RE-6GmL3gw`pSF{)H%Xn%z2s|lYcHz*?YBwUZyS@q{z0pQ zfBnr`q3{>e#c+(Zszk2o8;hjEwLcIPWnn_OWMIl$mf)!Q-uj57`hFToYu<`ZBIrLr zD|vNMMgK4L5)pBJbFrcu0dAljHkx0WCC@Cq91(G3oIz+9^$CFV5ol|C8RQPaZ!$c#h2jK!(#Zsi`&Fr#j@I z6;JV!A5|EUVQ@SioWL2{T>_^S*LZchM_6CXde>K#rCfDEXILrh%7V@if?Bx_YU6A2 zq;HJ-E*i_sxc|}912Vj!6^--CQ21WV3V3}lel6FoVVNWMjy$VCZk%mmlrW-Pn;zOU zf21||N_6aSbU9J*tSaDIbj!m9GT7+J@CsVKK^cM;Efb0xgzK_d0(WOoMS+kQnwlLI zt(j%$ zy-7LERLF1>rFCu18ii$aST8u7W7Y$0Tvs8hPTYOWR_rI7GH-$TWfMUFkbkp|87uR4 zMO$MumI2zzXkRnyFW0VNN`&9_jJY1Q#+f6ACyctiGL+eeDI!^%eFh$FAx*EXEF~7R zC=g0UCT$Errj}rtQKa;kTvg*uSoWz)005BG5_T)ixrLSnB?;2xl!bhmqciP-dDh$k zzzkPSP}MNwvV7P(`pM+0t}dx)(F1P9rJ{%iw-l0HyplrC;l2O#$tTZykKoxqc(wKK zrGo1AQo}pVT~bBFR*?>pSGGcGwXsk~P2y4oiDjbh0>ImHrj{xz)3iL*?yRGYPvMNl zYD24mBE^jK2>?r+__)Mr)g;g=0vmmSD+t~Xd~tQopH3kz_w2Q3S@1x0b$U64U#3+x z>PcVOo+Rq_$$x}1{sq|a{7^eADI`{Zh)DSvls$n%or8Z*_dhy#ia-M50e|^w|D`gJ zNhelgl+O$1)zsE_{DM&d>W{KQaWupJz*c>Q*NBQAyTg1cm0#KPBy|#`Z7^#= zgz)g-^TVEMlBWkBM=~X~cFX$OPfvcThoHKs6cd4QuY{p{sIEog)ss&?H7rbQYLt`O zq8!ydrH%ai&Q>rGp$ee(P%!VVrl>BI7E5Rn=n9(eU^2^aQOKA;pe^bc71U&_;rnxdW zjbtD0!j`zT#0>BaVF2dJiT6sN>`OwIZh?{4%)68-{rbi=TNkN~adq{SZzU~)ko382 z`DPo~nT^{z$EyMx%m?%BdppvFhSg6Xv(J$h@>;bcI2wo_x}ZxG>{FzP014j2O4U8x zLJtvY>C6&6U^bB%-k6BwJOZRNaagO1FIADplU{*?QJ{f5+#o;O$Wq$e-qe9EI7&XO zm)=T{?s<>cQxZ^nlq0X(^59ufdd9cxYVj4Ia|Q0z-)!%G;h?pMB~XR}NWk5`_XUDe zIEe$H4`8BA&C=l_LmmJPUm*`GhQ??(eU&`KDxb+}OyK2fAd4rJ&jx~8Id%k`Ia zSU@m>ic!>2#iSyAeD$+lWp1 z-M{%?^g}wgY5~}WFfni=waL|e`C7(@fCm+1Jgt~`40yeEG ziu$il3Z>w)`{g@YeQItBe#TP`(XG&@M^4j1Pa6O8-UX z`*$I+|3Fuoo++r(ee~*Bys0W!)~_@#%4ywUuG16sJ4+VVc!sr>3y!>)9XuaLgvu7|?45JZ(6I-##;5 z>P~$1#-I^#nCQ8YmHzR$t-%$5bC_-s5`vu_sadCITeeHoV?{Pj30)Q! z%%7$61WE2pR>3JlZzqCPm|6rT^We>$Q+Q7ZBcNQZ%vWa@*#AfU1Css zSKpk;*^y)pI=}+eowlGI^qzD`v4*=y5c#~lMRlbg@#?9kE8f)0rf|+B@n$Y9I7D6$ z9^b^rb`lKPLnq#`dIS9cl+vLVoXW=%3RNLvO|{VFTj|vaZIS18N|5lxs2?2~%9_`7 zp=++ExGNWB>+A3))anQOkn;sIxAC8M58IwekW4+IT0V%9NjCOc*co~)p^fj@*V6E{ z@;Jr-Q8zZ>OgBIC729of@rM1aGk67c$d{;@$)r>*JNpiP4~G>w)$~y<=#q3X>*DXu zQ2)yf+_sAWzIF93($Nk=mBVr|ks-~k|2(X95C|e-IvX%;Uyn}f!55B`2|-n_Q$n#F z>^kyIg)}l1^f4PR>t8%-RR4x1+Gm}_L^SBuhui~bD4_4PCGcGKc2isFAoEqTD-2~G zJm0m7oRVAW7cKMM>KWR&WWc-WLyBBiv0>Rr;=ysaS8KE6cHmy)vdVBSj-SK-gwUET5>7O>r4wKLnm2G$rR# z!O<-LJHvk} zAHWTISbvo*ID*$q0hBP?9KyXOgm{~}aak#%xhC}D3fPhcu^uEHRfPs7EUDNycJYX3 z!-?+WHk_dTIO=4)g=oHJ<~v2ker6lkDPJ1Qydc64Cq)Y}vRZ+^wJu z8zn+p;x>e5r@R^3$ldc}Bl@lnbhDV4rPtv5UU&C6`5*u7(QPcYom0Nr<{sX29Cm@_ zO?r{#3tT2a213^Ta3iWSnJ~R-^{5-)lA~aB2UfLwtU^sxJu@nRlmNHm=VXC49w&m7-( zpkb(cQzC{6Wrq|yAmmgGWjOaqwxhgUL&i{gnU3MbjR+Y^*HJRO45VBko(&eKp^8kX zXK&*URr$DF@>(nmT0n-Y9Mvn5VvE%u^CQlua;zoRBIT_HsN$HrbO1G?~u=89_h6*y=4x2a`j~8L{Ca@xHZE3bznd=}E8jhnx3x;#Foz+F6gyaWTBKaO) zjZ7i?0f+x2OhOic!2);1ACj2gqpHDLl8L~x@6_@%M3T+-icM^Ld#^~^?-g4#)sf=} z^@$d0wq!1kN$++YFUt-o=l(EWR;6c!nJcfq7dKa-d~bF}p#BK>8DXo;s$=L%p@^e0 z1(`ONSs2{6kr?D4R`$#>h|GB`BVz~>J@AQ1V8e!!`} z$GC+G>lCU1Y{N8caRY2y%@OE zmSQnR@Z^P1DJ-MpQDvDg@9Slq6E?=87&vfa__+)ESO#`)qN6BUMukc(okS@|(uSZ% z>;O;;$>TpZ9dbl_g^!y4;6}JcmXKK5NX}40;KbdB?D13E zi?k=-;hO=^{E#O)lH^Xa2TU{9);S}{H=a2{e=9G;qVvp;(?@-3j@o1QqODIR%<2z< z%F;t36f!+<<3jg(nb#giWV-(WiJCm-7C*PTQ7f+Y07*j)ECysKJSspPSU_N={Ot4g)_Rd zu*95n;Nx|YppZ%kXcW86*X3yvr|bEP5}G|C^IZZRDSds4Ba3{;E;s4$cTk&9TvSQyhBO*xsRBnW zADuP?uhftV_6ADQ+OJGw*XzlK`)_w3H9bYx-AYCQC?^P8G!;ZMwco;o0*D>q{9XBi zQ~BlM$TCY|%@MCWHyTGygJ$I!W&%OaljaFjGOLt_tZ~AZV}!L%<;-Q3LshYIK3R0! zGDoA_5`}>$2Q+KUu%$Ogb&_Amair?$k-MXo%ISSl9eAO4tYoapbu=3>!g0`2hj*#s z=DG;C$dW)`MIwZH%i&IV43)pIGW4ojV{d!`Z#_nGY@ab zS}iO{x3#cJYvX1r?_S*ed;5rwFaAz^Oth#<>v z1kNA1ECmbZ1iSQ;_S@482fcC$Bqwj;wnjDIwjw!Y!6gC!YE8R5HzE3H2OIB+6aJ$ye+%Ev(@*hrfIDQ zr3Xgf{uZXxF#;+V%9FHRpwok=lDF$-=WYCw@}to&UMUvz(JJ}N0c2nmwJl(@DL0R7qY8*EqKK@Dohr7$-03d z$Q3w#tzIZDox&lnn8`~Fl+ddfn2@38Cu0gCZ;Lh+@3@!et(Mg{%01P~C$C0_vWCQ! zLL7|5NA4A3Di>~=z$G{mMka|EG3~&qaEXnba(u0{74=R0p$ezZ!rYC(;{3}B{REel z&+|#c7u+R%8aNU>c3TKGQeQZs+`%19Sp9631d(pa8KNO94MQnhfYjgZGa@V(jO%PE z2fc@~QhkS-z!U`>a#@k#03+UI%;Z6qD7qon7^(uXapY-^2Uob-02gR#Y+lW_etz0d zj7MP~Ja&i=MkSxocGRK0@>E^HMY|cc%ZnRfT_+mxwWycC3`ZnzILsM@&#Ml!CTBk@Y}CPSyW$6tuCp|>POYDcirUlK1dY|Wwn;A>oyjU6~JxSI1?E+oJX)@5~!lV4d>sMy|?Nl zC=7TQbkI1aXFS-$cYYEwVg_;IET;-)p|L@ul876QOBUsApnMZJi?b;5rryHEDtA+& zJe-FnoCZV6>{8631bO{?c{Y=J0By{;QtDGNV{W8u!>heQT)(|4tOQWrRsvcPW;Cb3 zDp&y~3-^eg!^Qm?%8l^ldg5y-xDKq~u^_Q8-t02J)%yukB3{e&5 zP$T3sfIfU{Py~o-WWQ>Asdv$~Coj>yskaQRdL0T!4UJqrmFJO9(CfAR%gr<9&??qG zocPgM>hO~p^QDzu^i3!|L{d~<#Xh2oSngL3=a<_{u2T@!!074YUO5Eg?aCavYZX)M z32hCPA5J-_#Ro#hQ+{j1Y|_jS%tZcGk~HyBrAO~|$4}0a$fLp~KiN!9N>>~NWz$A7 z6y=64Z_0b5i$zp6y{tpbW^6J556+Y6{YWMsGH(j=&aUDQXSXiumtg2HEi*=8PQnhT z02;zrQBXsfeG|i?+qUDek@TWHkmhlEHNr}wVWo45^7_I|vl#9$aXs`}4PVr5#Ec`H z6XSa;*MEg@axLSiPEsAm;99f!t4Q?AW6! zJugaY3yUiJrbKcI;7V*S+1^cLgC|WfZbpUsPyX_^zVO2!**)8jN0dq~b?ic0z>{`a zt|5`PML64&l_S{mHNfM8@Z z2}=;u<>9Mr#7%>|7ej8EWnnqs3r6Sa z1~9wo_{h!>RX9S}C2Vl71PwrQ1ps<`i;oPs;7ZXj_$l}C36#>Zbb3+sL6Aj`#bOOF zbkiM%tx*=jLWz$%&6MHnoc50F3dE?oqM)BF(n4>%QgfNRq9jefzFz`ETN5O8ETK6w zQBJ#}EddM5MxFJkRF%^aI(D~+H^NE+FMy%{LO`Ji6wPI5LLOoB#0i=5-GL4r8HEcKBxtPLC7w615$Q3+13GX?O0&?{{N?~`Lpl0lj zJU_CeRCAYM1H-x;4zbrqXHX$nBQ{P9L|j$swd?!h6{tn7kZd1M$&pW(k z=i$yq8SUz8MNVouz>#-A3->)@^kZCrwscs)_eedFPvED3zecu4tIGm+z0nzI4o=4( z?4X?B=$SLopKJetbBOnRZ_)dq#nH}r`bz_FihWvGl;PiX5Zt?jBKa5_wsY z>+0BZRCUjpFP3D@9MlTI%yxg#NoMz!PGpNAD;S;thQpt#ClJCW#3_Dqwp+6M3nAk@ zYR|~MrgjLYNw(%gaBX){we8D)E`~TXw}L}}zm`Gud^E>6%XcSTtPwDYkFc;%Z;fDz zd%^I)_OCebS*n`CuGLxr?P^@op%ntxM^y6=r~dT5Ht?`77OP*l>b<7~a!ViP_vj80 zU}KL$Icj*-S%RwF+sb9T2qoRqp#BmZ_8|f^L@Y?*;9K>Tg{}4Na%h?@huDo4@t;*F znir)dED!6)-2W@RBNV2L+iQ3ce;mE>aHU~j!RfdJ#K-F5Tcxf19X~Hl7$nnD`eV7sd@#Jo;6Twb_N%5) zSz!6)(G zr&0odtjFL)v=y1TXlId`ur%(ODvpB}WS9K&}$Fy%uA6xUJvauF%J()+c2` z2tVEY^C!F0=j9U;MUc|qCN84O<_t>0_JU0si3P4znC0iV^>3VR7gxAl`8;|5 zeYvw%JegR2!Cl8d2NDBt9tw)d!MFt6(AtY~00ExO+cX-GGan>a32MTKc}VYJ;!+|M z36A;7Gc}L_I35lZJLC6H39Or5!kb>SOJLd_($)0zfh@<-1K2Rmta$18CbuV*#px^C zlt_>q6jA0?vlY?v*x5b4@Nki@c%-lC2oWGVjur;*=>Q%>P+D?eY0L^ewpdjR6JEl; zRA2g17F82nEPAoM9wd|wA?a51j%Jmp9Pu}w=Cd9ih;Lny=BCsS#8NSru~=YKK~e#} z@tO+vKGcPwv`*_w%HsYNBnX%EBqBQ31YD4z8mE3FKz%*V5;|4dlc>Fw?C1Eb!I7qAx0}PIBQ6uByj%oZ{V= z_S$HEshDId@c@#ZJN$LsC^GZQ7#}2OF+WQ(79u1+#%HUe;SME1>H4PxOE|y?gvbzd zj*no1gbjVCzyTP)=whw|9Q zg|9O#BR~(e^x~W1F_XxkqzL1hSKxo5e_8X!D612bMG1yx?=Au1*^CeE;#&6r>Pp;t^y{G!h zY9`5-$u`2V_R}?eD10U_{IU6h#&{?^#otD#MmT?@p^e4Nt`au?8Vv-lp!z;+LM{*b z*x7x5 zXuSgi;IJ*8_(E^1wE5Xd;6D`c-p&W-x3Fy zA#Vp)*dwk1Oc4}hRR}0H$=l62%|*(y)0)}ZfCXeRfngK@MQ0PmqOpEq5!T32!a-#TD4J1_Li0m16ozL+`Zs^i{>h zi)2Nt-#I#SSkS^wm`XCl7|J52#hhjJcw~0v6;A+y9I+aflkm#C+3DtCNuKi?1&gu=$+pCm8%j$cQru1qtm?2Gfr;FmWJ(ayFf;!fYM}yf&-dg&cYkS94uwZW_Te2`MCiD-OlO^~V zS3?)Vgu%WaAx%upJ}$;&p&(6I3LWcLJ{t+s0M2m;laY<03quMvfO>QsLH+wPD^-E9 z@&nG$d?AD1*H+L1G)Ax+$d30ETth%qfNPM`yH%lpgiSRP&c=)a77br+o@*zKr6qE@ zB~FKlE$>o-g@2X)y9(GRq)V-q zB#+=LUhp6ckb9V7AeRyFw&s68ikTR)MRk0Viyi}pR7uO)j3xMN1UMOU1rS+ck62u* zxXv&D%;JEn342hyDs#_O*0puAQnFQ$U16up*%M}WRO>Q--RcU$UJ&UU)q`ml=aD+1_^R%ahdST5G7$l&XnA+(xRzC+Gv$T-44%f#=g=cKo@Kni zjZ)~1sF#|Sd@z{IMHv!nw7?3CSR>SG0qp;i+>P36&`KZX}vCf3Il*xf$3j>|5S66(mA+HkEmP*+7Vf3F#=^#XEBNT`U9E8IEy@k z12R$G^YCKgn5YFth=T zib|VijU+*7C<^g{+u1%Wrte&B(X!!_2GztUr!yX!5)Rc*w=9*4;q1|XIS!gP z+*=**L`xfQb@Xqi*d-7!Xtu0EUDX zVHj9ZiydX)JFsJsn_(x@A*G_`BXcmZ12n@<e zNu@H>xLgW}YhY>Y$(RmFM~u>;wsanLW#BbMMC-pn?1Hy`mPl{tHu~Z$o0`K9^9xTv zKmwc}-woa)jw-G|rZ3HOF&^w|jto1;*xG#1>ex5!Ksno5@&V%?j^E0)pe1sI9Bd7n z&8>G8eNwzJU|)14H#qbU>jbzHE>1}+4)(|(P!W-+dGk~D%hDm0O_VE|!Do}|BlDK71c9T;-q~lgO>@gx&r6twW zB%ob{%d;OONbb1>Ruv_FT9uyijuk@kF{g8!&*t?(1_QYittj3QPEDwRk1g6@HBI!q zzbR%Z;D=7Z34pQQ%0{g{-|iRqK~uTSS3-Axy^JsRVVZBHCJx00%dRbEGIO9jRfRHL z8==leh6rYTOT1pGC^~nwA|GF0QKsb8ihR6O(akaa+xTeJ!KzuoX5xZ7O`4}Vz9M&U zDQD&4?-I%D5v_Jc}ZJ9Hj)n0Zm!K=DL{oc;zs} z32w(TsB5wU0Fp$xxaNb%Bh%^uGCjgh3{ii&x+W5_B-RTpK`(`46qbk2)bGzDQ0Yl4 z&vj1xa_E3CHYv}pkO{~1*yS#Vcj&lOz?ciqICr%$64ZT;3AS?5D}sYK4HXLxJ6*aw zv^fAMH48SOp?x(!t-PEX&EtBcUG}#EFE~4wPLA~ho#FY!ygYAP;8EwCwjSTSn0O$7 ztqMAr+;OznZ(HvbzilBABOK?9kr&Xd9Oqmc9s_kGq!X_Gz-57xWD}g(6boWA8}S+t zo$T0K;~9I0y7(o+TUpFwc=L7iadS#5W2+91bkie{LZH-ui6A?dI8|8-c;xFAiV+b+ z!<)%DPY{9)HB9GkxoLJ+u)g>64~+*Uj`#8In{OQ6!=xhj9~4Y3O$o zxefsd$~bufcjcZVBGTs|;}Zj2!h?3wD|Fe6&fI(;8)2+)^t4vnC{Dksw2BXWb{X-4c)5{!U%)FDM`R5Dg)EO!fOJc zu?W)~s{y&xO_M%DJynY1JUnXPOxl6rmI+G+fA*Gxh zB)tkjLfgX5hs6|$Wkwhq*4DKuM@`vSj19``)NHbWdbPBY(BCQ8t0kbDV=x@i{c`T6 zc()ZXI9^tUKXAk59n>)ic6(bMs+}z;xTQ^+;$1{(Dd~fUa$o9}L>hFKlAIhX6X&B;0hQNTMM`e#;4IKu*hmWdg~fV89zS(6Z0 zT3dFK#lzwKY@`QbOm~MKK%kUz%=^}rB`a6(#oGlnV_lJ&JlR`jUED>LQTV=-uObcWT5q=PLIHTg%>r);LC zi_{L4peSrY3^tuhKE|WjPij|jwx%c-=pFhgtM=+*HW&*M;8U-z@r72{j;hH(6wT=l zF%tI$Q!UY-iw)ZzK7oyC=U^?SQ}LD3uR!-3(s(D_ZK5`)_MH?u+tbBUB{hNL#8+(; zJ&K*&cPM3X#zF)d5E9xzkZ40{`Gc_%k%S0OkGO` z**N=3Pbp|5w>SoZJ|x@#4M%3ih!qIEq-ngjFsJJ{YMYjAA?hfZR{6@`dhE>DwCIX^pVj33e8vvc%H3otSy)2!B$vUEYwJBVbMj zvmtd5_Bo7U%OL!zX=LI@!UjrU<~Vmsc=B}!K8O1@j~#lpL{+V!0{l4x%1X?x~w zJY#0s>)Qwk1&I@i_!3AwBEcKt6Y#)0-+?FKE#is#(>>E;`?7y>iVg`!z8!a0S65e6 zSN*Cc$3MBbRxaTu`s7)EP2B$TuizQKua>@7a%U=*Pvx*VkoWuRrJKL}5Wop?OQ;)A*P9Jgt6<1dEkwuN<|ur(M-(}DINurX`HA>FcpSkAcY$y()G?o z5Q&ML#L0P?DMWbYC^wzN%5}ncykaznMzJ+K4@QAAPTZ+qKF*Hu??3&z|M;Je|MK^* zluG#hCLF_Jj4aG)p8c*jcx}~b7U;z=bknq4ra!in$V}|ONHcT?GAs*m<|bm2j`fo# zL-)K4KRh{t;|8iabrTt>8Vm`M&ZaV{rZODWn#+?g+)Sl%K%vUl>zbfiJy6p;4B0eJ ztF5|d)o?rDZN^h|9!h60c7|~nCsh!YA6ida^r_WR{NwgX-#Kh|kHjzV=V`y)?>PIN zgZBMie_^AHVLWxKTP@i0^6Mw@8OY)epNx}u7J1dxtmDL!iG%F7-+JpR>``4TUgCPe zj1+Qt;qui}*K^!3tZrX#^8p`Vg~H!`x6`?TK9VAlX)05kkqDpWVx_wPpVaY+47C3BKH_7!pN1nmWb%ulL z8;w1>UIRIb6X^hbm6g|d&^hYEwdr;G9lVNg4>v?2$3d!OA|B=RkA#Xb5)Q%3B*aW# zMj{?OmO~|i6oN%4mGnqj9S@r*fR_YkCSbs%j64%sW>e2qQeP+3Iz`4{YV=SMuM5Ko zr>eE)D1iI78B90pVso7u;oZ@i2wtRHU*minWrkL z>b380`qG^`BQVeAhb(|GQ7|$aK(LCG8$@Y!bDAdZgvr@h0<#Bg@j94Wh~v`}cd|og zC~RXbT-XOSh`~|^Z8(k58>r)h2nE(#uq%LEc%mQfa(IPjTCSrlA9i67gUSj=JZ?GB)Mr@bLhmlz2OtE%ZRsg-e#5}wXvsdAMQ29czs=gVO1 zt14u@j4lm6l2t#EDO~P%APv0f#{hCmfVmecOfI?kQ<+DCUg`&2_@=DP;3{=?qonVEpSBHdW><&X-(n-K8NI@v-aN_{yc#Z4gGrp;)h1=X=RjqFykL1MI>mDv#i zU>ZvSc+U{h!ZQlUCWeGAgc%arP}y%hEMmwY8bWkWB~fAuzz={=5$v#OvlY!|#;w7S z0ZDUZL7@rQTVbgPUaL7VDgdcN5GP~x<9rz@6dBQH0}$%-<;m8ADA~TqV6{AK_$uUX zW}eBSu~~ry7{#zJD!FJM$eMK_&vcoM&4~aMU)kH+!y;ta<+UO(J!9$TgJLzr!+XiW z1}8!}0{dO2TGpDPKY)6%7%#F)2+n+dUb@!gP;wDF@gwciMe*Jw)tl1;5M6zG6;EH# z;Rmr&@#Fm;suF12T@0Em`Yf& zj)67WX*HYMltDCZhhrI|=b3|UHT>j00*+Jiz>U|bT0@z|?yiUeXnjHqe}V#GNRMIx ztxt&P@fZU-P-l@YZDv8yzNpwrKnN>FL6R!6_tL`YEcGeFXbH^OR5GKZadcgWFwZlz zrzsmrR|Sok8Q90Jk}XN0bQwY;2dibvCOKvyd;~SIF$I}pxp)LEEVOWc>sLOCzgjBc z_v>7s%$lO=3?NaW;Ed=E^VIS4$GLuloQc_#2~3}tO6%*@ zSHMdfJnuTbBt2S7pqHYLiJIETZo2M1TP@BT3uvlacL^o-0%!;zZe$4As<&$E>l8yMCKfkP=TnP+|KV}R>2;4f z%(6(z+NA2t>f?8h_Pcl5{d*@hh*Mr1Z7NJFb9hvYJr}G&l2Jn+$_k7$dAqjT?;M^k z9ELZgMVw@PhjJ-xv-mvAG6oZ@{ zlg%uvKhNqQjkFBF+CUw0kF zqm)73qx+W_ujWzr887PF11x|3oy`l%TVIU71DrhPLMj)M7nIXa#HfL@) zlZ-Gh57o^YzE_;mDX;Q-#uYG!E-Y*QlC9*GXV!ul6EcEZ+W+&nlu6nAUbom#wk9x7 zgYIv&%J789{T+OwOUK5d_1s#XrxpSS>N+m20XYq09}3&Z4P=D{IDdZsG5#&QQh$YCsV}@vw;3+K&bAr;6&Nmb(+L{^U&KbA z!FRa{Zu9*Ipe~Cqz%#=ZTjHO=7R4<>H=FGz-zK)X84cH!!oKol)YlsP_W=G!gF8RK GU;hRDME=MC literal 0 HcmV?d00001 diff --git a/tasmota/berry/openhasp/pages.jsonl b/tasmota/berry/openhasp/pages.jsonl new file mode 100644 index 000000000..59578d84a --- /dev/null +++ b/tasmota/berry/openhasp/pages.jsonl @@ -0,0 +1,33 @@ +{"page":0,"comment":"---------- Upper stat line ----------"} +{"id":0,"text_color":"#FFFFFF"} +{"id":11,"obj":"label","x":0,"y":0,"w":320,"pad_right":90,"h":22,"bg_color":"#D00000","bg_opa":255,"radius":0,"border_side":0,"text":"Tasmota","text_font":"montserrat-20"} + +{"id":15,"obj":"lv_wifi_arcs","x":291,"y":0,"w":29,"h":22,"radius":0,"border_side":0,"bg_color":"#000000","line_color":"#FFFFFF"} +{"id":16,"obj":"lv_clock","x":232,"y":3,"w":55,"h":16,"radius":0,"border_side":0} + +{"comment":"---------- Bottom buttons - prev/home/next ----------"} +{"id":101,"obj":"btn","x":20,"y":210,"w":80,"h":25,"action":"prev","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF053","text_font":"montserrat-20"} +{"id":102,"obj":"btn","x":120,"y":210,"w":80,"h":25,"action":"back","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF015","text_font":"montserrat-20"} +{"id":103,"obj":"btn","x":220,"y":210,"w":80,"h":25,"action":"next","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF054","text_font":"montserrat-20"} + +{"page":2,"comment":"---------- Page 2 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"comment":"---------- Wifi status ----------"} +{"id":20,"obj":"lv_wifi_graph","x":257,"y":25,"w":60,"h":40,"radius":0} +{"id":21,"obj":"lv_tasmota_info","x":3,"y":25,"w":251,"h":40,"radius":0} +{"id":22,"obj":"lv_tasmota_log","x":3,"y":68,"w":314,"h":90,"radius":0,"text_font":12} + +{"page":1,"comment":"---------- Page 1 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"id":2,"obj":"arc","x":20,"y":65,"w":80,"h":100,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":0,"value_ofs_y":-14,"bg_opa":0,"text":"--.-°C","min":200,"max":800,"val":0,"val_rule":"ESP32#Temperature","val_rule_formula":"val * 10","text_rule":"ESP32#Temperature","text_rule_format":"%2.1f °C"} + +{"id":5,"obj":"label","x":2,"y":35,"w":120,"text":"Temperature","align":1} + +{"id":10,"obj":"label","x":172,"y":35,"w":140,"text":"MPU","align":0} +{"id":11,"obj":"label","x":172,"y":55,"w":140,"text":"x=","align":0,"text_rule":"MPU9250#AX","text_rule_format":"x=%6.3f","text_rule_formula":"val / 1000"} +{"id":12,"obj":"label","x":172,"y":75,"w":140,"text":"y=","align":0,"text_rule":"MPU9250#AY","text_rule_format":"y=%6.3f","text_rule_formula":"val / 1000"} +{"id":13,"obj":"label","x":172,"y":95,"w":140,"text":"z=","align":0,"text_rule":"MPU9250#AZ","text_rule_format":"z=%6.3f","text_rule_formula":"val / 1000"} + +{"comment":"--- Trigger sensors every 2 seconds ---","berry_run":"tasmota.add_cron('*/2 * * * * *', def () tasmota.publish_rule(tasmota.read_sensors()) end, 'oh_every_5_s')"} diff --git a/tasmota/berry/openhasp/robotocondensed_latin1.tapp b/tasmota/berry/openhasp/robotocondensed_latin1.tapp new file mode 100644 index 0000000000000000000000000000000000000000..e4c8ca84ceeb371d83f63d49a0ae2d1f47679b1e GIT binary patch literal 61585 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 UtTt2dWc+`(CO(P%7 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 openhasp.start()) diff --git a/tasmota/berry/openhasp_src/openhasp_core/openhasp.be b/tasmota/berry/openhasp_src/openhasp_core/openhasp.be new file mode 100644 index 000000000..85bae288f --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_core/openhasp.be @@ -0,0 +1,1489 @@ +# OpenHASP compatibility module +# +# use `import openhasp` and set the JSONL definitions in `pages.jsonl` +# +# As an optimization `0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#` is replaced with `0` +# +var openhasp = module("openhasp") + +################################################################################# +################################################################################# +# Class `lvh_obj` encapsulating `lv_obj`` +# +# Provide a mapping for virtual members +# Stores the associated page and object id +# +# Adds specific virtual members used by OpenHASP +################################################################################# +################################################################################# +class lvh_obj + static _lv_class = lv.obj # _lv_class refers to the lvgl class encapsulated, and is overriden by subclasses + static _lv_part2_selector # selector for secondary part (like knob of arc) + + # attributes to ignore when set at object level (they are managed by page) + static _attr_ignore = [ + "tostring", # avoid issues with Berry `tostring` method + # "id", + "obj", + "page", + "comment", + "parentid", + "auto_size", # TODO not sure it's still needed in LVGL8 + # attributes for page + "prev", "next", "back", + "berry_run", # run Berry code after the object is created + ] + + # The following defines the mapping between the JSONL attribute name + # and the Berry or LVGL attribute to set + # + # We try to map directly an attribute to the LVGL + # Ex: OpenHASP attribute `w` is mapped to LVGL `width` + # + # If mapping is null, we use set_X and get_X from our own class + static _attr_map = { + "x": "x", + "y": "y", + "w": "width", + "h": "height", + # arc + "asjustable": nil, + "mode": nil, + "start_angle": "bg_start_angle", + "start_angle1": "start_angle", + "end_angle": "bg_end_angle", + "end_angle1": "end_angle", + "radius": "style_radius", + "border_side": "style_border_side", + "border_width": "style_border_width", + "bg_opa": "style_bg_opa", + "border_width": "style_border_width", + "line_width": nil, # depends on class + "line_width1": nil, # depends on class + "action": nil, # store the action in self.action + "hidden": nil, # apply to self + "enabled": nil, # apply to self + "click": nil, # synonym to enabled + "toggle": nil, + "bg_color": "style_bg_color", + "bg_opa": "style_bg_opa", + "bg_grad_color": "style_bg_grad_color", + "bg_grad_dir": "style_bg_grad_dir", + "line_color": "style_line_color", + "pad_left": "style_pad_left", + "pad_right": "style_pad_right", + "pad_top": "style_pad_top", + "pad_bottom": "style_pad_bottom", + "pad_all": "style_pad_all", # write-only + "type": nil, + # below automatically create a sub-label + "text": nil, # apply to self + "value_str": nil, # synonym to 'text' + "align": nil, + "text_font": nil, + "value_font": nil, # synonym to text_font + "text_color": nil, + "value_color": nil, # synonym to text_color + "value_ofs_x": nil, + "value_ofs_y": nil, + # + "min": nil, + "max": nil, + "val": "value", + "rotation": "rotation", + # img + "src": "src", + "image_recolor": "style_img_recolor", + "image_recolor_opa": "style_img_recolor_opa", + # spinner + "angle": nil, + "speed": nil, + # padding of knob + "pad_top2": nil, + "pad_bottom2": nil, + "pad_left2": nil, + "pad_right2": nil, + "pad_all2": nil, + "radius2": nil, + # rule based update of attributes + # supporting both `val` and `text` + "val_rule": nil, + "val_rule_formula": nil, + "text_rule": nil, + "text_rule_formula": nil, + "text_rule_format": nil, + } + + #==================================================================== + # Instance variables + var id # (int) object hasp id + var _lv_obj # native lvgl object + var _lv_label # sub-label if exists + var _page # parent page object + var _action # value of the OpenHASP `action` attribute, shouldn't be called `self.action` since we want to trigger the set/member functions + + #==================================================================== + # Rule engine to map value and text to rules + # hence enabling auto-updates ob objects + var _val_rule # rule pattern to map the `val` attribute + var _val_rule_formula # Berry fragment to transform the value grabbed from rule + var _val_rule_function # compiled function + var _text_rule # rule pattern to map the `text` attribute + var _text_rule_formula # Berry fragment to transform the value grabbed from rule before string format + var _text_rule_function # compiled function + var _text_rule_format # string format to transform the value grabbed from rule + + ################################################################################# + # General utilities + # + ################################################################################# + # Checks if the attribute is a color + # I.e. ends with `color` (to not conflict with attributes containing `color_`) + ################################################################################# + static def is_color_attribute(t) + import re + return bool(re.search("color$", str(t))) + end + + ################################################################################# + # Parses a color attribute + # + # `parse_color(hex:string) -> color:int` (as 24 bits RGB int) + # + # Parses colors in multiple forms: + # - `0xRRGGBB` + # - `#RRGGBB` + # - `` that are matched to `lv.COLOR_` (ex: `red`) - case insensitive + # - defaults to black `0x000000` if parsing fails + ################################################################################# + static def parse_color(s) + # inner function + def parse_hex(s) + # parse hex string + # parse_hex(string) -> int + # skip any `#` prefix, or `0x` and `0X` prefix + import string + s = string.toupper(s) # turn to uppercase + var val = 0 + for i:0..size(s)-1 + var c = s[i] + # var c_int = string.byte(c) + if c == "#" continue end # skip '#' prefix if any + if c == "x" || c == "X" continue end # skip 'x' or 'X' + + if c >= "A" && c <= "F" + val = (val << 4) | string.byte(c) - 55 + elif c >= "0" && c <= "9" + val = (val << 4) | string.byte(c) - 48 + end + end + return val + end + + s = str(s) + if s[0] == '#' + return lv.color(parse_hex(s)) + else + import string + import introspect + var col_name = "COLOR_" + string.toupper(s) + var col_try = introspect.get(lv, col_name) + if col_try != nil + return lv.color(col_try) + end + end + # fail safe with black color + return lv.color(0x000000) + end + + #==================================================================== + # init OpenHASP object from its jsonl definition + # + # arg1: LVGL parent object (used to create a sub-object) + # arg2: `jline` JSONL definition of the object from OpenHASP template (used in sub-classes) + # arg3: (opt) LVGL object if it already exists and was created prior to init() + #==================================================================== + def init(parent, page, jline, obj) + self._page = page + if obj == nil && self._lv_class + var obj_class = self._lv_class # assign to a var to distinguish from method call + self._lv_obj = obj_class(parent) # instanciate LVGL object + else + self._lv_obj = obj + end + self.post_init() + end + + #==================================================================== + # post-init, to be overriden and used by certain classes + #==================================================================== + def post_init() + self.register_event_cb() + end + + ##################################################################### + # General Setters and Getters + ##################################################################### + + #==================================================================== + # get LVGL encapsulated object + #==================================================================== + def get_obj() + return self._lv_obj + end + + #==================================================================== + # Value of the `action` attribute + #==================================================================== + def set_action(t) + self._action = str(t) + # add callback when clicked + # TODO + # self._lv_obj.add_event_cb(/ obj, event -> self.action_cb(obj, event), lv.EVENT_CLICKED, 0) + end + def get_action() + return self._action + end + + #==================================================================== + # Add cb for any action on the object + # + # Below is the mapping between HASP and LVGL (may need to adjust) + # down = LV_EVENT_PRESSED + # up = LV_EVENT_CLICKED + # lost = LV_EVENT_PRESS_LOST + # release = LV_EVENT_RELEASED + # long = LV_EVENT_LONG_PRESSED + # hold = LV_EVENT_LONG_PRESSED_REPEAT + # changed = LV_EVENT_VALUE_CHANGED + #==================================================================== + static _event_map = { + lv.EVENT_PRESSED: "down", + lv.EVENT_CLICKED: "up", + lv.EVENT_PRESS_LOST: "lost", + lv.EVENT_RELEASED: "release", + lv.EVENT_LONG_PRESSED: "long", + lv.EVENT_LONG_PRESSED_REPEAT: "hold", + lv.EVENT_VALUE_CHANGED: "changed", + } + def register_event_cb() + # register callback for each event + var f = / obj, event -> self.event_cb(obj, event) + for ev:self._event_map.keys() + self._lv_obj.add_event_cb(f, ev, 0) + end + # # print("register_event_cb") + # var mask = lv.EVENT_PRESSED | lv.EVENT_CLICKED | lv.EVENT_PRESS_LOST | lv.EVENT_RELEASED | + # lv.EVENT_LONG_PRESSED | lv.EVENT_LONG_PRESSED_REPEAT | lv.EVENT_VALUE_CHANGED + # var target = self + # mask = lv.EVENT_CLICKED + # self._lv_obj.add_event_cb(/ obj, event -> target.event_cb(obj, event), mask, 0) + end + + def event_cb(obj, event) + # the callback avoids doing anything sophisticated in the cb + # defer the actual action to the Tasmota event loop + # print("-> CB fired","self",self,"obj",obj,"event",event.tomap(),"code",event.code) + var oh = self._page._oh # openhasp global object + var code = event.code # materialize to a local variable, otherwise the value can change (and don't capture event object) + if self.action != nil && code == lv.EVENT_CLICKED + # if clicked and action is declared, do the page change event + tasmota.set_timer(0, /-> oh.do_action(self, code)) + end + + var event_hasp = self._event_map.find(code) + if event_hasp != nil + import string + var val = string.format('{"hasp":{"p%ib%i":"%s"}}', self._page._page_id, self.id, event_hasp) + # var pxby = "p" + self._page._page_id + "b" + self.id + # var val = '{"hasp":{"p' + str(self._page._page_id) + 'b' + str(self.id) + + # '":"' + event_hasp + '"}}' + # var val = json.dump( {'hasp': {pxby: event_hasp}} ) + # print("val=",val) + tasmota.set_timer(0, /-> tasmota.publish_rule(val)) + end + end + + # def action_cb(obj, event) + # # the callback avoids doing anything sophisticated in the cb + # # defer the actual action to the Tasmota event loop + # # print("-> CB fired","self",self,"obj",obj,"event",event.tomap(),"code",event.code) + # var oh = self._page._oh # openhasp global object + # var code = event.code # materialize to a local variable, otherwise the value can change (and don't capture event object) + # tasmota.set_timer(0, /-> oh.do_action(self, code)) + # end + + #==================================================================== + # Mapping of synthetic attributes + # - text + # - hidden + # - enabled + #==================================================================== + #- `hidden` attributes mapped to OBJ_FLAG_HIDDEN -# + def set_hidden(h) + if h + self._lv_obj.add_flag(lv.OBJ_FLAG_HIDDEN) + else + self._lv_obj.clear_flag(lv.OBJ_FLAG_HIDDEN) + end + end + def get_hidden() + return self._lv_obj.has_flag(lv.OBJ_FLAG_HIDDEN) + end + + #==================================================================== + # `enabled` attributes mapped to OBJ_FLAG_CLICKABLE + #==================================================================== + def set_enabled(h) + if h + self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) + else + self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) + end + end + def get_enabled() + return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) + end + + #==================================================================== + # click is synonym to enabled + #==================================================================== + def set_click(t) self.set_enabled(t) end + def get_click() return self.get_enabled() end + + #==================================================================== + # line_width + #==================================================================== + def set_line_width(t) + self._lv_obj.set_style_line_width(int(t), 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def get_line_width() + return self._lv_obj.get_style_line_width(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + + #==================================================================== + # `toggle` attributes mapped to STATE_CHECKED + #==================================================================== + def set_toggle(t) + import string + t = string.toupper(str(t)) + if t == "TRUE" t = true end + if t == "FALSE" t = false end + if t + self._lv_obj.add_state(lv.STATE_CHECKED) + else + self._lv_obj.clear_state(lv.STATE_CHECKED) + end + end + def get_toggle() + return self._lv_obj.has_state(lv.STATE_CHECKED) + end + + #==================================================================== + # `adjustable` flag + #==================================================================== + def set_adjustable(t) + if t + self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) + else + self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) + end + end + def get_adjustable() + return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) + end + + #==================================================================== + # set_text: create a `lv_label` sub object to the current object + # (default case, may be overriden by object that directly take text) + #==================================================================== + def check_label() + if self._lv_label == nil + self._lv_label = lv.label(self.get_obj()) + self._lv_label.set_align(lv.ALIGN_CENTER); + end + end + def set_text(t) + self.check_label() + self._lv_label.set_text(str(t)) + end + def set_value_str(t) self.set_text(t) end + def get_text() + if self._lv_label == nil return nil end + return self._lv_label.get_text() + end + def get_value_str() return self.get_text() end + + # mode + def set_mode(t) + var mode + if t == "expand" self._lv_obj.set_width(lv.SIZE_CONTENT) + elif t == "break" mode = lv.LABEL_LONG_WRAP + elif t == "dots" mode = lv.LABEL_LONG_DOT + elif t == "scroll" mode = lv.LABEL_LONG_SCROLL + elif t == "loop" mode = lv.LABEL_LONG_SCROLL_CIRCULAR + elif t == "crop" mode = lv.LABEL_LONG_CLIP + end + if mode != nil + self.check_label() + self._lv_label.set_long_mode(mode) + end + end + def get_mode() + end + + #==================================================================== + # `align`: `left`, `center`, `right` + #==================================================================== + def set_align(t) + var align + self.check_label() + if t == 0 || t == "left" + align = lv.TEXT_ALIGN_LEFT + elif t == 1 || t == "center" + align = lv.TEXT_ALIGN_CENTER + elif t == 2 || t == "right" + align = lv.TEXT_ALIGN_RIGHT + end + self._lv_label.set_style_text_align(align, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + + def get_align() + if self._lv_label == nil return nil end + var align self._lv_label.get_style_text_align(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + if align == lv.TEXT_ALIGN_LEFT + return "left" + elif align == lv.TEXT_ALIGN_CENTER + return "center" + elif align == lv.TEXT_ALIGN_RIGHT + return "right" + else + return nil + end + end + + #==================================================================== + # `text_font` + # + # For OpenHASP compatiblity, default to "robotocondensed-latin1" + # However we propose an extension to allow for other font names + # + # Arg1: (int) font size for `robotocondensed-latin1` + # or + # Arg1: (string) "font_name-font_size", ex: "montserrat-20" + #==================================================================== + def set_text_font(t) + # self.check_label() + var font + if type(t) == 'int' + font = lv.font_robotocondensed_latin1(t) + elif type(t) == 'string' + import string + var fn_split = string.split(t, '-') + if size(fn_split) >= 2 # it does contain '-' + var sz = int(fn_split[-1]) + var name = fn_split[0..-2].concat('-') # rebuild the font name + if sz > 0 && size(name) > 0 # looks good, let's have a try + try + font = lv.font_embedded(name, sz) + except .. + end + end + end + end + if font != nil + self._lv_obj.set_style_text_font(font, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + else + print("HSP: Unsupported font:", t) + end + end + def get_text_font() + end + def set_value_font(t) self.set_text_font(t) end + def get_value_font() return self.get_text_font() end + + #==================================================================== + # `text_color` + #==================================================================== + def set_text_color(t) + self._lv_obj.set_style_text_color(self.parse_color(t), 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def get_text_color() + return self._lv_obj.get_style_text_color(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def set_value_color(t) self.set_text_color(t) end + def get_value_color() return self.get_value_color() end + + #==================================================================== + # `ofs_x`, `ofs_y` + #==================================================================== + def set_value_ofs_x(t) + self.check_label() + self._lv_label.set_x(int(t)) + end + def get_value_ofs_x() + return self._lv_label.get_x() + end + def set_value_ofs_y(t) + self.check_label() + self._lv_label.set_y(int(t)) + end + def get_value_ofs_y() + return self._lv_label.get_y() + end + + #==================================================================== + # `pad_top2`, `pad_bottom2`, `pad_left2`, `pad_right2`, `pad_alL2` + # secondary element + #==================================================================== + def set_pad_top2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_top(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_bottom2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_bottom(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_left2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_left(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_right2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_right(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_all2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_all(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + + #==================================================================== + # `pad_top`, `pad_bottom`, `pad_left`, `pad_right`, `pad_all` + #==================================================================== + def get_pad_top() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_top(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_bottom() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_bottom(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_left() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_left(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_right() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_right(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_all() + end + + #==================================================================== + # `radius2` + #==================================================================== + def set_radius2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_radius(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_radius2() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_radius(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + + #- ------------------------------------------------------------# + # Internal utility functions + # + # Mapping of virtual attributes + # + #- ------------------------------------------------------------# + # `member` virtual getter + #- ------------------------------------------------------------# + def member(k) + import string + # ignore attributes + # print("member","self=",self,"k=",k) + if self._attr_ignore.find(k) != nil return end + + # check if the key is known + if self._attr_map.contains(k) + # attribute is known + # kv: (if string) the LVGL attribute name of the object - direct mapping + # kv: (if `nil`) call `get_` method of the object + import introspect + var kv = self._attr_map[k] + + if kv == nil + # call the object's `get_X()` + var f = introspect.get(self, "get_" + k) # call self method + if type(f) == 'function' + return f(self) + end + else + # call the native LVGL object method + var f = introspect.get(self._lv_obj, "get_" + kv) + if type(f) == 'function' # found and function, call it + if string.find(kv, "style_") == 0 + # style function need a selector as second parameter + return f(self._lv_obj, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + else + return f(self._lv_obj) + end + end + end + end + # fallback to exception if attribute unknown or not a function + raise "value_error", "unknown attribute " + str(k) + end + + #- ------------------------------------------------------------# + # `setmember` virtual setter + #- ------------------------------------------------------------# + def setmember(k, v) + # print(">> setmember", k, v) + # print(">>", classname(self), self._attr_map) + # ignore attributes + if self._attr_ignore.find(k) != nil return end + + # is attribute known + if self._attr_map.contains(k) + import string + import introspect + var kv = self._attr_map[k] + # if a string is attached to the name, then set the corresponding LVGL attribute + if kv + var f = introspect.get(self._lv_obj, "set_" + kv) + # if the attribute contains 'color', convert to lv_color + if type(kv) == 'string' && self.is_color_attribute(kv) + v = self.parse_color(v) + end + # print("f=", f, v, kv, self._lv_obj, self) + if type(f) == 'function' + if string.find(kv, "style_") == 0 + # style function need a selector as second parameter + f(self._lv_obj, v, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + else + f(self._lv_obj, v) + end + return + else + print("HSP: Could not find function set_"+kv) + end + else + # else call the specific method from self + var f = introspect.get(self, "set_" + k) + # print("f==",f) + if type(f) == 'function' + f(self, v) + return + end + end + + else + print("HSP: unknown attribute:", k) + end + end + + #==================================================================== + # Rule based updates of `val` and `text` + # + # `val_rule`: rule pattern to grab a value, ex: `ESP32#Temperature` + # `val_rule_formula`: formula in Berry to transform the value + # Ex: `val * 10` + # `text_rule`: rule pattern to grab a value for text, ex: `ESP32#Temparature` + # `text_rule_format`: format used by `string.format()` + # Ex: `%.1f °C` + #==================================================================== + def set_val_rule(t) + # remove previous rule if any + if self._val_rule != nil + tasmota.remove_rule(self._val_rule, self) + end + + self._val_rule = str(t) + tasmota.add_rule(self._val_rule, / val -> self.val_rule_matched(val), self) + end + def get_val_rule() + return self._val_rule + end + # text_rule + def set_text_rule(t) + # remove previous rule if any + if self._text_rule != nil + tasmota.remove_rule(self._text_rule, self) + end + + self._text_rule = str(t) + tasmota.add_rule(self._text_rule, / val -> self.text_rule_matched(val), self) + end + def get_text_rule() + return self._text_rule + end + def set_text_rule_format(t) + self._text_rule_format = str(t) + end + def get_text_rule_format() + return self._text_rule_format + end + # formula that gets compiled as Berry code + def set_val_rule_formula(t) + self._val_rule_formula = str(t) + var code = "return / val -> (" + self._val_rule_formula + ")" + try + var func = compile(code) + self._val_rule_function = func() + except .. as e, m + import string + print(string.format("HSP: failed to compile '%s' - %s (%s)", code, e, m)) + end + end + def get_val_rule_formula() + return self._val_rule_formula + end + # formula that gets compiled as Berry code + def set_text_rule_formula(t) + self._text_rule_formula = str(t) + var code = "return / val -> (" + self._text_rule_formula + ")" + try + var func = compile(code) + self._text_rule_function = func() + except .. as e, m + import string + print(string.format("HSP: failed to compile '%s' - %s (%s)", code, e, m)) + end + end + def get_text_rule_formula() + return self._text_rule_formula + end + # rule matched for val + def val_rule_matched(val) + + # print(">> rule matched", "val=", val) + var val_n = real(val) # force float type + var func = self._val_rule_function + if func != nil + try + val_n = func(val_n) + except .. as e, m + import string + print(string.format("HSP: failed to run self._val_rule_function - %s (%s)", e, m)) + end + end + + self.val = int(val_n) # set value, truncate to int + return false # propagate the event further + end + # rule matched for text + def text_rule_matched(val) + + # print(">> rule matched text", "val=", val) + var val_n = real(val) # force float type + + var func = self._text_rule_function + if func != nil + try + val_n = func(val_n) + except .. as e, m + import string + print(string.format("HSP: failed to run self._text_rule_function - %s (%s)", e, m)) + end + end + + var format = self._text_rule_format + if type(format) == 'string' + import string + format = string.format(format, val_n) + else + format = "" + end + + self.text = format + return false # propagate the event further + end +end + +################################################################################# +# +# Other widgets +# +################################################################################# + +#==================================================================== +# label +#==================================================================== +class lvh_label : lvh_obj + static _lv_class = lv.label + # label do not need a sub-label + def post_init() + self._lv_label = self._lv_obj # the label is also the object itself + super(self).post_init() # call super + end +end + +#==================================================================== +# arc +#==================================================================== +class lvh_arc : lvh_obj + static _lv_class = lv.arc + static _lv_part2_selector = lv.PART_KNOB + + # line_width converts to arc_width + def set_line_width(t) + self._lv_obj.set_style_arc_width(int(t), 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def get_line_width() + return self._lv_obj.get_arc_line_width(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def set_line_width1(t) + self._lv_obj.set_style_arc_width(int(t), lv.PART_INDICATOR | lv.STATE_DEFAULT) + end + def get_line_width1() + return self._lv_obj.get_arc_line_width(lv.PART_INDICATOR | lv.STATE_DEFAULT) + end + + def set_min(t) + self._lv_obj.set_range(int(t), self.get_max()) + end + def set_max(t) + self._lv_obj.set_range(self.get_min(), int(t)) + end + def get_min() + return self._lv_obj.get_min_value() + end + def get_max() + return self._lv_obj.get_max_value() + end + def set_type(t) + var mode + if t == 0 mode = lv.ARC_MODE_NORMAL + elif t == 1 mode = lv.ARC_MODE_REVERSE + elif t == 2 mode = lv.ARC_MODE_SYMMETRICAL + end + if mode != nil + self._lv_obj.set_mode(mode) + end + end + def get_type() + return self._lv_obj.get_mode() + end +end + +#==================================================================== +# switch +#==================================================================== +class lvh_switch : lvh_obj + static _lv_class = lv.switch + static _lv_part2_selector = lv.PART_KNOB +end + +#==================================================================== +# spinner +#==================================================================== +class lvh_spinner : lvh_arc + static _lv_class = lv.spinner + + # init + # - create the LVGL encapsulated object + # arg1: parent object + # arg2: json line object + def init(parent, page, jline) + self._page = page + var angle = jline.find("angle", 60) + var speed = jline.find("speed", 1000) + self._lv_obj = lv.spinner(parent, speed, angle) + self.post_init() + end + + # ignore attributes, spinner can't be changed once created + def set_angle(t) end + def get_angle() end + def set_speed(t) end + def get_speed() end +end + +################################################################################# +# +# All other subclasses than just map the LVGL object +# and doesn't have any specific behavior +# +################################################################################# +class lvh_bar : lvh_obj static _lv_class = lv.bar end +class lvh_btn : lvh_obj static _lv_class = lv.btn end +class lvh_btnmatrix : lvh_obj static _lv_class = lv.btnmatrix end +class lvh_checkbox : lvh_obj static _lv_class = lv.checkbox end +class lvh_dropdown : lvh_obj static _lv_class = lv.dropdown end +class lvh_img : lvh_obj static _lv_class = lv.img end +class lvh_line : lvh_obj static _lv_class = lv.line end +class lvh_roller : lvh_obj static _lv_class = lv.roller end +class lvh_slider : lvh_obj static _lv_class = lv.slider end +class lvh_textarea : lvh_obj static _lv_class = lv.textarea end +# special case for scr (which is actually lv_obj) +class lvh_scr : lvh_obj static _lv_class = nil end # no class for screen + + +################################################################################# +# Class `lvh_page` +# +# Encapsulates a `lv_screen` which is `lv.obj(0)` object +################################################################################# +# +# ex of transition: lv.scr_load_anim(scr, lv.SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false) +class lvh_page + var _obj_id # (map) of `lvh_obj` objects by id numbers + var _page_id # (int) id number of this page + var _lv_scr # (lv_obj) lvgl screen object + var _oh # OpenHASP global object + # openhasp attributes for page are on item `#0` + var prev, next, back # (int) id values for `prev`, `next`, `back` buttons + + #==================================================================== + # `init` + # + # arg1: `page_number` (int) OpenHASP page id + # defaults to `1` if not specified + # page 0 is special, visible on all pages. Internally uses `layer_top` + # arg2: `oh` global OpenHASP monad object + # page_number: openhasp page number, defaults to `1` if not specified + #==================================================================== + def init(page_number, oh) + import global + self._oh = oh # memorize OpenHASP parent object + + # if no parameter, default to page #1 + page_number = int(page_number) + if page_number == nil page_number = 1 end + + self._page_id = page_number # remember our page_number + self._obj_id = {} # init list of objects + + # initialize the LVGL object for the page + # uses a lv_scr object except for page 0 where we use layer_top + # page 1 is mapped directly to the default screen `scr_act` + if page_number == 1 + self._lv_scr = lv.scr_act() # default screen + elif page_number == 0 + self._lv_scr = lv.layer_top() # top layer, visible over all screens + else + self._lv_scr = lv.obj(0) # allocate a new screen + var bg_color = lv.scr_act().get_style_bg_color(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) # bg_color of default screen + self._lv_scr.set_style_bg_color(bg_color, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) # set white background + end + + # page object is also stored in the object map at id `0` as instance of `lvg_scr` + var obj_scr = lvh_scr(nil, self, nil, self._lv_scr) # store screen in a virtual object + self._obj_id[0] = obj_scr + + # create a global for this page of form p, ex `p1` + # create a global for the page attributes as pb0, ex `p1b0` + global.("p" + str(self._page_id)) = self + global.("p" + str(self._page_id) + "b0") = obj_scr + end + + ##################################################################### + # General Setters and Getters + ##################################################################### + + #==================================================================== + # retrieve lvgl screen object for this page + #==================================================================== + def get_scr() + return self._lv_scr + end + + #==================================================================== + # return id of this page + #==================================================================== + def id() + return self._page_id + end + + #==================================================================== + # add an object to this page + #==================================================================== + def set_obj(id, o) + self._obj_id[id] = o + end + def get_obj(id) + return self._obj_id.find(id) + end + + #==================================================================== + # `show` transition from one page to another + # duration: in ms, default 500 ms + # anim: -1 right to left, 1 left to right (default) + # + # show this page, with animation + #==================================================================== + static show_anim = { + 1: lv.SCR_LOAD_ANIM_MOVE_LEFT, + -1: lv.SCR_LOAD_ANIM_MOVE_RIGHT, + -2: lv.SCR_LOAD_ANIM_MOVE_TOP, + 2: lv.SCR_LOAD_ANIM_MOVE_BOTTOM, + 0: lv.SCR_LOAD_ANIM_NONE, + } + def show(anim, duration) + # ignore if the page does not contain a screen, like when id==0 + if self._lv_scr == nil return nil end + + # ignore if the screen is already active + # compare native LVGL objects with current screen + if self._lv_scr._p == lv.scr_act()._p return end # do nothing + + # default duration of 500ms + if duration == nil duration = 500 end + + # if anim is `nil` try to guess the direction from current screen + if anim == nil + anim = self._oh.page_dir_to(self.id()) + end + + # change current page + self._oh.lvh_page_cur_idx = self._page_id + + var anim_lvgl = self.show_anim.find(anim, lv.SCR_LOAD_ANIM_NONE) + # load new screen with animation, no delay, 500ms transition time, no auto-delete + lv.scr_load_anim(self._lv_scr, anim_lvgl, duration, 0, false) + end +end + +################################################################################# +# +# class `OpenHASP` to initialize the OpenHASP parsing +# +################################################################################# + +# main class controller, meant to be a singleton and the only externally used class +class OpenHASP + var dark # (bool) use dark theme? + var hres, vres # (int) resolution + var scr # (lv_obj) default LVGL screen + var r16, r20 # (lv_font) robotocondensed fonts size 16 and 20 + # openhasp objects + var lvh_pages # (list of lvg_page) list of pages + var lvh_page_cur_idx # (int) current page index number + # regex patterns + var re_page_target # compiled regex for action `p` + + # assign lvh_page to a static attribute + static lvh_page = lvh_page + # assign all classes as static attributes + static lvh_btn = lvh_btn + static lvh_switch = lvh_switch + static lvh_checkbox = lvh_checkbox + static lvh_label = lvh_label + # static lvh_led = lvh_led + static lvh_spinner = lvh_spinner + static lvh_obj = lvh_obj + static lvh_line = lvh_line + static lvh_img = lvh_img + static lvh_dropdown = lvh_dropdown + static lvh_roller = lvh_roller + static lvh_btnmatrix = lvh_btnmatrix + # static lvh_msgbox = lvh_msgbox + # static lvh_tabview = lvh_tabview + # static lvh_tab = lvh_tab + # static lvh_cpiker = lvh_cpiker + static lvh_bar = lvh_bar + static lvh_slider = lvh_slider + static lvh_arc = lvh_arc + # static lvh_linemeter = lvh_linemeter + # static lvh_gauge = lvh_gauge + static lvh_textarea = lvh_textarea # additional? + + static def_templ_name = "pages.jsonl" # default template name + + def init() + import re + self.re_page_target = re.compile("p\\d+") + # nothing to put here up to now + end + + def deinit() + # remove previous rule if any + if self._val_rule != nil + tasmota.remove_rule(self._val_rule, self) + end + if self._text_rule != nil + tasmota.remove_rule(self._text_rule, self) + end + end + + #==================================================================== + # init + # + # arg1: (bool) use dark theme if `true` + # + # implicitly loads `pages.jsonl` from file-system // TODO allow to specicify file name + #==================================================================== + def start(dark, templ_name) + import path + if templ_name == nil templ_name = self.def_templ_name end + if !path.exists(templ_name) + raise "io_erorr", "file '" + templ_name + "' not found" + end + # start lv if not already started. It does no harm to call lv.start() if LVGL was already started + lv.start() + + self.dark = bool(dark) + + self.hres = lv.get_hor_res() # ex: 320 + self.vres = lv.get_ver_res() # ex: 240 + self.scr = lv.scr_act() # LVGL default screean object + + self.r20 = lv.font_robotocondensed_latin1(20) # // TODO what if does not exist + self.r16 = lv.font_robotocondensed_latin1(16) # // TODO what if does not exist + + # set the theme for OpenHASP + var th2 = lv.theme_openhasp_init(0, lv.color(0xFF00FF), lv.color(0x303030), self.dark, self.r16) + self.scr.get_disp().set_theme(th2) + self.scr.set_style_bg_color(self.dark ? lv.color(0x000000) : lv.color(0xFFFFFF),0) # set background to white + # apply theme to layer_top, but keep it transparent + lv.theme_apply(lv.layer_top()) + lv.layer_top().set_style_bg_opa(0,0) + + self.lvh_pages = {} + # load from JSONL + self._load(templ_name) + end + + ################################################################################# + # Simple insertion sort - sorts the list in place, and returns the list + ################################################################################# + static def sort(l) + # insertion sort + for i:1..size(l)-1 + var k = l[i] + var j = i + while (j > 0) && (l[j-1] > k) + l[j] = l[j-1] + j -= 1 + end + l[j] = k + end + return l + end + + + ##################################################################### + # General Setters and Getters + ##################################################################### + + #==================================================================== + # return the current page as `lvh_page` object + #==================================================================== + def get_page_cur() + return self.lvh_pages[self.lvh_page_cur_idx] + end + + #==================================================================== + # load JSONL template + #==================================================================== + def _load(templ_name) + import string + import json + #- pages -# + self.lvh_page_cur_idx = 1 + var lvh_page_class = self.lvh_page + self.lvh_pages[1] = lvh_page_class(1, self) # always create page #1 + + var f = open(templ_name,"r") + var jsonl = string.split(f.read(), "\n") + f.close() + + # parse each line + for j:jsonl + var jline = json.load(j) + + if type(jline) == 'instance' + self.parse_page(jline) # parse page first to create any page related objects, may change self.lvh_page_cur_idx + # objects are created in the current page + self.parse_obj(jline, self.lvh_pages[self.lvh_page_cur_idx]) # then parse object within this page + end + end + + # current page is always 1 when we start + self.lvh_page_cur_idx = 1 + end + + #==================================================================== + # `parse` + # + # Manually parse a single JSON line, after initial load + #==================================================================== + def parse(j) + import json + var jline = json.load(j) + + if type(jline) == 'instance' + self.parse_page(jline) # parse page first to create any page related objects, may change self.lvh_page_cur_idx + # objects are created in the current page + self.parse_obj(jline, self.lvh_pages[self.lvh_page_cur_idx]) # then parse object within this page + else + raise "value_error", "unable to parse JSON line" + end + end + + #==================================================================== + # `pages_list_sorted` + # + # Return the sorted list of page (ids without page 0) starting + # from the current page. + # Ex: if pages are [0,1,3,4,5,6] + # pages_list_sorted(4) -> [4,5,6,1,3] + # + # Arg1: number of current page, or `0` for current page, or `nil` to return just the list of pages + # Returns: list of ints, or nil if current page is not found + #==================================================================== + def pages_list_sorted(cur_page) + # get list of pages as sorted array + var pages = [] + if cur_page == 0 cur_page = self.lvh_page_cur_idx end + for p: self.lvh_pages.keys() + if p != 0 pages.push(p) end # discard page 0 + end + pages = self.sort(pages) + if cur_page == nil return cur_page end + + var count_pages = size(pages) # how many pages are defined + pages = pages + pages # double the list to splice it + var cur_idx = pages.find(cur_page) + if cur_idx == nil return nil end # internal error, current page not found + + pages = pages[cur_idx .. cur_idx + count_pages - 1] # splice the list + + return pages + end + + #==================================================================== + # `page_dir_to` + # + # Compute the best direction (right or left) to go from + # the current page to the destination page + # + # Returns: + # 1: scroll to the next page (right) + # 0: unknown + # -1: scroll to the prev page (left) + # -2: scroll to the home page (up or left) + #==================================================================== + def page_dir_to(to_page) + var sorted_pages_list = self.pages_list_sorted(0) # list of pages sorted by number, page 0 excluded + if sorted_pages_list == nil return 0 end + + var count_pages = size(sorted_pages_list) # how many pages are possible + if count_pages <= 1 return 0 end + # if we have 2 pages, then only 1 direction is possible + if count_pages == 2 return 1 end + # we have at least 3 pages + + var to_page_idx = sorted_pages_list.find(to_page) # find index of target page + if to_page_idx == nil return 0 end # target page not found + if to_page_idx <= (count_pages + 1) / 2 + return 1 + else + return -1 + end + end + + #==================================================================== + # Execute a page changing action from `action` attribute + # + # This is called in async mode after a button callback + # + # Arg1: lvh_X object that fired the action + # Arg2: LVGL event fired + # Returns: nil + #==================================================================== + def do_action(lvh_obj, event_code) + if event_code != lv.EVENT_CLICKED return end + var action = lvh_obj._action + var cur_page = self.lvh_pages[self.lvh_page_cur_idx] + # print("do_action","lvh_obj",lvh_obj,"action",action,"cur_page",cur_page,self.lvh_page_cur_idx) + + # action can be `prev`, `next`, `back`, or `p` like `p1` + var to_page = nil + var sorted_pages_list = self.pages_list_sorted(self.lvh_page_cur_idx) + if size(sorted_pages_list) <= 1 return end # if only 1 page, do nothing + # handle prev/next/back values + # get the corresponding value from page object, + # if absent, revert to next page, previous page and page 1 + # print("sorted_pages_list",sorted_pages_list) + if action == 'prev' + to_page = int(cur_page.prev) + if to_page == nil to_page = sorted_pages_list[-1] end # if no prev, take the previous page + elif action == 'next' + to_page = int(cur_page.next) + if to_page == nil to_page = sorted_pages_list[1] end # if no next, take the next page + elif action == 'back' + to_page = int(cur_page.back) + if to_page == nil to_page = 1 end # if no nack, take page number 1 + elif self.re_page_target.match(action) + # action is supposed to be `p` format + to_page = int(action[1..-1]) # just skip first char and convert the rest to a string + end + + # print("to_page=",to_page) + if to_page != nil && to_page > 0 # we have a target + self.lvh_pages[to_page].show() # switvh to the target page + end + end + + #==================================================================== + # Parse page information + # + # Create a new page object if required + # Change the active page + #==================================================================== + def parse_page(jline) + if jline.has("page") && type(jline["page"]) == 'int' + var page = int(jline["page"]) + self.lvh_page_cur_idx = page # change current page + + # create the page object if it doesn't exist already + if !self.lvh_pages.contains(page) + var lvh_page_class = self.lvh_page + self.lvh_pages[page] = lvh_page_class(page, self) + end + + # check if there is "id":0 + if jline.find("id") == 0 + var lvh_page_cur = self.get_page_cur() + lvh_page_cur.prev = int(jline.find("prev", nil)) + lvh_page_cur.next = int(jline.find("next", nil)) + lvh_page_cur.back = int(jline.find("back", nil)) + end + end + end + + #==================================================================== + # Parse single object + #==================================================================== + def parse_obj(jline, page) + import global + import string + import introspect + + var obj_id = int(jline.find("id")) # id number or nil + var obj_type = str(jline.find("obj")) # obj class or nil + var obj_lvh # lvgl object created + var lvh_page_cur = self.get_page_cur() # current page object + + # first run any Berry code embedded + var berry_run = str(jline.find("berry_run")) + if berry_run != "nil" + try + var func_compiled = compile(berry_run) + # run the compiled code once + func_compiled() + except .. as e,m + print(string.format("HSP: unable to run berry code \"%s\" - '%s' - %s", berry_run, e, m)) + end + end + + # if line contains botn 'obj' and 'id', create the object + if obj_id == nil return end # if no object id, ignore line + if obj_type != "nil" && obj_id != nil + # 'obj_id' must be between 1 and 254 + if obj_id < 1 || obj_id > 254 + print("HSP: invalid 'id': " + str(obj_id) + " for 'obj':" + obj_type) + return + end + + # extract openhasp class, prefix with `lvh_`. Ex: `btn` becomes `lvh_btn` + # extract parent + var parent_lvgl + var parent_id = int(jline.find("parentid")) + + if parent_id != nil + var parent_obj = lvh_page_cur.get_obj(parent_id) # get parent object + if parent_obj != nil parent_lvgl = parent_obj._lv_obj end # parent + end + if parent_lvgl == nil + parent_lvgl = lvh_page_cur.get_scr() # if not parent, use the current page screen + end + + # check if a class with the requested name exists + # first look for a class with name `lvh_` exists + var obj_class = introspect.get(self, "lvh_" + obj_type) + var lv_instance = nil # allows to pre-instanciate the object + + # there is no lvh_X class, try to load the class name from the global namespace + if obj_class == nil + # if not found, check if a LVGL class with name `lv_` exists + var lv_cl = introspect.get(global, obj_type) + if lv_cl != nil && type(lv_cl) == 'class' + lv_instance = lv_cl(parent_lvgl) + obj_class = lvh_obj # use the basic lvh_obj component to encapsulate + end + end + + # still not found, try to load a module with the name of the class + if obj_class == nil + var lv_cl = introspect.module(obj_type) + if lv_cl != nil && type(lv_cl) == 'class' + lv_instance = lv_cl(parent_lvgl) + obj_class = lvh_obj # use the basic lvh_obj component to encapsulate + end + end + + if obj_class == nil + print("HSP: cannot find object of type " + str(obj_type)) + return + end + + # instanciate the object, passing the lvgl screen as parent object + obj_lvh = obj_class(parent_lvgl, page, jline, lv_instance) + + # add object to page object + lvh_page_cur.set_obj(obj_id, obj_lvh) + + # create a global variable for this object of form pb, ex p1b2 + var glob_name = string.format("p%ib%i", lvh_page_cur.id(), obj_id) + global.(glob_name) = obj_lvh + end + + if obj_id == 0 && obj_type != "nil" + print("HSP: cannot specify 'obj' for 'id':0") + return + end + + # if id==0, retrieve the 'scr' object of the current page + if obj_id == 0 + obj_lvh = self.get_page_cur().get_obj(0) # get object id '0' + end + + # set attributes + # try every attribute, if not supported it is silently ignored + for k:jline.keys() + # introspect.set(obj, k, jline[k]) + obj_lvh.(k) = jline[k] + end + end +end +openhasp.OpenHASP = OpenHASP + +################################################################################# +# General module initilization +################################################################################# + +# automatically instanciate the OpenHASP() monad +# note: value is cached in the module cache +# and is returned whenever you call `import openhasp` again +# This means that the object is never garbage collected +# +openhasp.init = def (m) # `init(m)` is called during first `import openhasp` + return openhasp.OpenHASP() +end + +return openhasp diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo-all.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo-all.jsonl similarity index 100% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo-all.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo-all.jsonl diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo1.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo1.jsonl similarity index 99% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo1.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo1.jsonl index 684e0d324..bc34ee5fb 100644 --- a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo1.jsonl +++ b/tasmota/berry/openhasp_src/openhasp_examples/demo1.jsonl @@ -1,5 +1,6 @@ {"page":1,"comment":"---------- Page 1 ----------"} {"page":1,"id":0,"bg_color":"#FFFFFF","bg_grad_color":"#FFFFFF","text_color":"#000000","radius":0,"border_side":0} + {"page":1,"id":1,"obj":"btn","x":0,"y":0,"w":240,"h":30,"text":"LIVING ROOM","value_font":22,"bg_color":"#2C3E50","bg_grad_color":"#2C3E50","text_color":"#FFFFFF","radius":0,"border_side":0} {"page":1,"id":2,"obj":"arc","x":20,"y":65,"w":80,"h":100,"max":40,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":0,"value_ofs_y":-14,"bg_opa":0,"text":"21.2°C","min":-20,"max":50,"val":21} diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo2.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo2.jsonl similarity index 100% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo2.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo2.jsonl diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo3.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo3.jsonl similarity index 100% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo3.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo3.jsonl diff --git a/tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl new file mode 100644 index 000000000..a9af406b9 --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl @@ -0,0 +1,44 @@ +{"page":0,"comment":"---------- Upper stat line ----------"} +{"id":0,"text_color":"#FFFFFF"} +{"id":11,"obj":"label","x":0,"y":0,"w":320,"pad_right":90,"h":22,"bg_color":"#D00000","bg_opa":255,"radius":0,"border_side":0,"text":"Tasmota","text_font":"montserrat-20"} + +{"id":15,"obj":"lv_wifi_arcs","x":291,"y":0,"w":29,"h":22,"radius":0,"border_side":0,"bg_color":"#000000","line_color":"#FFFFFF"} +{"id":16,"obj":"lv_clock","x":232,"y":3,"w":55,"h":16,"radius":0,"border_side":0} + +{"comment":"---------- Bottom buttons - prev/home/next ----------"} +{"id":101,"obj":"btn","x":20,"y":210,"w":80,"h":25,"action":"prev","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF053","text_font":"montserrat-20"} +{"id":102,"obj":"btn","x":120,"y":210,"w":80,"h":25,"action":"back","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF015","text_font":"montserrat-20"} +{"id":103,"obj":"btn","x":220,"y":210,"w":80,"h":25,"action":"next","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF054","text_font":"montserrat-20"} + +{"page":2,"comment":"---------- Page 2 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"comment":"---------- Wifi status ----------"} +{"id":20,"obj":"lv_wifi_graph","x":257,"y":25,"w":60,"h":40,"radius":0} +{"id":21,"obj":"lv_tasmota_info","x":3,"y":25,"w":251,"h":40,"radius":0} +{"id":22,"obj":"lv_tasmota_log","x":3,"y":68,"w":314,"h":90,"radius":0,"text_font":12} + +{"page":1,"comment":"---------- Page 1 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"id":2,"obj":"arc","x":20,"y":65,"w":80,"h":100,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":0,"value_ofs_y":-14,"bg_opa":0,"text":"--.-°C","min":200,"max":800,"val":0,"val_rule":"ESP32#Temperature","val_rule_formula":"val * 10","text_rule":"ESP32#Temperature","text_rule_format":"%2.1f °C"} + +{"id":5,"obj":"label","x":2,"y":35,"w":140,"text":"Temperature","align":1} + +{"id":10,"obj":"label","x":172,"y":35,"w":140,"text":"MPU","align":0} +{"id":11,"obj":"label","x":172,"y":55,"w":140,"text":"x=","align":0,"text_rule":"MPU9250#AX","text_rule_format":"x=%6.3f","text_rule_formula":"val / 1000"} +{"id":12,"obj":"label","x":172,"y":75,"w":140,"text":"y=","align":0,"text_rule":"MPU9250#AY","text_rule_format":"y=%6.3f","text_rule_formula":"val / 1000"} +{"id":13,"obj":"label","x":172,"y":95,"w":140,"text":"z=","align":0,"text_rule":"MPU9250#AZ","text_rule_format":"z=%6.3f","text_rule_formula":"val / 1000"} + + +{"page":3,"comment":"---------- Page 3 ----------"} +{"page":3,"id":1,"obj":"btn","x":0,"y":20,"w":240,"h":30,"text":"PAGE 3","text_font":16,"bg_color":"#2C3E50","text_color":"#FFFFFF","radius":0,"border_side":0,"click":0} + +{"page":3,"id":11,"obj":"img","src":"A:/noun_Fan_35097_140.png","auto_size":1,"w":140,"h":140,"x":50,"y":75,"image_recolor":"lime","image_recolor_opa":150} +{"page":3,"id":12,"obj":"spinner","parentid":11,"x":7,"y":6,"w":126,"h":126,"bg_opa":0,"border_width":0,"line_width":7,"line_width1":7,"type":2,"angle":120,"speed":1000,"value_str":3,"value_font":24} + +{"page":4,"comment":"---------- Page 4 ----------"} +{"page":4,"id":1,"obj":"btn","x":0,"y":20,"w":240,"h":30,"text":"PAGE 4","value_font":24,"bg_color":"#2C3E50","text_color":"#FFFFFF","radius":0,"border_side":0,"click":0} +{"page":4,"id":2,"obj":"obj","x":5,"y":35,"w":230,"h":250,"click":0} + +{"comment":"--- Trigger sensors every 2 seconds ---","berry_run":"tasmota.add_cron('*/2 * * * * *', def () tasmota.publish_rule(tasmota.read_sensors()) end, 'oh_every_5_s')"} diff --git a/tasmota/berry/openhasp_src/openhasp_widgets.tapp b/tasmota/berry/openhasp_src/openhasp_widgets.tapp new file mode 100644 index 0000000000000000000000000000000000000000..45212df04b3b0cf12b418c6b9475107751ea88b9 GIT binary patch literal 8367 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 GU;hRDME=MC literal 0 HcmV?d00001 diff --git a/tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be b/tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be new file mode 100644 index 000000000..edada704e --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be @@ -0,0 +1,6 @@ +# pre-load widgets so future `import` will be already in memory +# create tapp file with: +# rm rm openhasp_widgets.tapp; zip -j -0 openhasp_widgets.tapp openhasp_widgets/* +import lv_tasmota_log +import lv_tasmota_info +import lv_wifi_graph diff --git a/tasmota/berry/lvgl_examples/lv_tasmota_info.be b/tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_info.be similarity index 100% rename from tasmota/berry/lvgl_examples/lv_tasmota_info.be rename to tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_info.be diff --git a/tasmota/berry/lvgl_examples/lv_tasmota_log.be b/tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_log.be similarity index 87% rename from tasmota/berry/lvgl_examples/lv_tasmota_log.be rename to tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_log.be index 0bf2b42de..8db31f48b 100644 --- a/tasmota/berry/lvgl_examples/lv_tasmota_log.be +++ b/tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_log.be @@ -24,14 +24,18 @@ class lv_tasmota_log : lv.obj self.refr_pos() self.label = lv.label(self) - self.label.set_width(self.get_width() - 12) self.label.set_style_text_color(lv.color(0x00FF00), lv.PART_MAIN | lv.STATE_DEFAULT) self.label.set_long_mode(lv.LABEL_LONG_CLIP) self.label.set_text("") # bug, still displays "Text" - self.add_event_cb( / obj, evt -> self.size_changed_cb(obj, evt), lv.EVENT_SIZE_CHANGED | lv.EVENT_STYLE_CHANGED | lv.EVENT_DELETE, 0) + self.label.set_width(self.get_width() - 12) + self.label.set_height(self.get_height() - 6) + self.add_event_cb( / -> self._size_changed(), lv.EVENT_SIZE_CHANGED, 0) + self.add_event_cb( / -> self._size_changed(), lv.EVENT_STYLE_CHANGED, 0) + self.add_event_cb( / -> tasmota.remove_driver(self), lv.EVENT_DELETE, 0) + self.lines = [] self.line_len = 0 self.log_reader = tasmota_log_reader() @@ -54,7 +58,7 @@ class lv_tasmota_log : lv.obj self.line_len = line_len end - def _size_changed() + def _size_changed(obj, evt) # print(">>> lv.EVENT_SIZE_CHANGED") var pad_hor = self.get_style_pad_left(lv.PART_MAIN | lv.STATE_DEFAULT) + self.get_style_pad_right(lv.PART_MAIN | lv.STATE_DEFAULT) @@ -64,7 +68,7 @@ class lv_tasmota_log : lv.obj + self.get_style_pad_bottom(lv.PART_MAIN | lv.STATE_DEFAULT) + self.get_style_border_width(lv.PART_MAIN | lv.STATE_DEFAULT) * 2 + 3 - var w = self.get_width() - pad_hor + var w = self.get_width() - pad_hor - 2 var h = self.get_height() - pad_ver self.label.set_size(w, h) # print("w",w,"h",h,"pad_hor",pad_hor,"pad_ver",pad_ver) @@ -76,15 +80,6 @@ class lv_tasmota_log : lv.obj self.set_lines_count(lines_count) end - def size_changed_cb(obj, event) - var code = event.code - if code == lv.EVENT_SIZE_CHANGED || code == lv.EVENT_STYLE_CHANGED - self._size_changed() - elif code == lv.EVENT_DELETE - tasmota.remove_driver(self) - end - end - def every_second() var dirty = false for n:0..20 diff --git a/tasmota/berry/lvgl_examples/lv_wifi_graph.be b/tasmota/berry/openhasp_src/openhasp_widgets/lv_wifi_graph.be similarity index 100% rename from tasmota/berry/lvgl_examples/lv_wifi_graph.be rename to tasmota/berry/openhasp_src/openhasp_widgets/lv_wifi_graph.be