diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e3e5764b..b748904c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: [ 'python', 'typescript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support diff --git a/HMI/n2t-out/pageIcons.txt b/HMI/n2t-out/pageIcons.txt index 0c8e6312..9effdb49 100644 --- a/HMI/n2t-out/pageIcons.txt +++ b/HMI/n2t-out/pageIcons.txt @@ -72,7 +72,7 @@ Text tIcons Horizontal Alignment : left Vertical Alignment : top Input Type : character - Text :  + Text :  Max. Text Size : 120 Word wrap : enabled Horizontal Spacing : 0 diff --git a/README.md b/README.md index e536fefa..45e6dd7f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ NsPanel Lovelace UI is a Firmware for the nextion screen inside of NSPanel in th ## Features - Entities Page with support for cover, switch, input_boolean, sensor, button, input_button and light -- Detail Pages for Lights (Brightness and Temperature of the Light) and for Covers (Position) +- Detail Pages for Lights (Brightness, Temperature and Color of the Light) and for Covers (Position) - Thermostat Page - Media Player Card - Screensaver Page with Time, Date and Weather Information @@ -84,6 +84,13 @@ The easiest way to install it is through Home Assistant's Supervisor Add-on Stor ![hass-add-on-store](doc-pics/hass-add-on-store.png) +#### Add babel package to AppDaemon Container (Optional) + +For localisation (date in your local language) you need to add the python package babel to your AppDaemon Installation. + +![appdaemon-babel](doc-pics/appdaemon-babel.png) + + ### Installing Studio Code Server (optional, recommended) You will need a way to edit the `apps.yaml` config file in the Appdaemon folder. @@ -138,8 +145,8 @@ directory of your AppDaemon installation. ## Installation - Home Automation Part (IoBroker) -If you are looking for ioBroker Integration instead of HomeAssistant take a look into the [Readme](ioBroker/README.md) in the iobroker folder. -Thanks to @britzelpuf for this integration. +If you are looking for an ioBroker Integration instead of HomeAssistant take a look into the [Readme](ioBroker/README.md) of the iobroker folder. +Thanks to [britzelpuf](https://github.com/britzelpuf) for this integration. ## Installation - NSPanel Part @@ -251,9 +258,11 @@ nspanel-1: value: 10 - time: "23:00:00" value: 0 - locale: "de_DE" + locale: "de_DE" # only used if babel python package is installed + dateFormatBabel: "full" # only used if babel python package is installed + # formatting options on https://babel.pocoo.org/en/latest/dates.html?highlight=name%20of%20day#date-fields timeFormat: "%H:%M" - dateFormat: "%A, %d. %B %Y" + dateFormat: "%A, %d. %B %Y" # ignored if babel python package is installed weatherEntity: weather.example pages: - type: cardEntities diff --git a/appdaemon/apps.yaml b/appdaemon/apps.yaml index 3c8e5efe..92e6c4bd 100644 --- a/appdaemon/apps.yaml +++ b/appdaemon/apps.yaml @@ -12,9 +12,11 @@ nspanel: value: 10 - time: "23:00:00" value: 0 - locale: "de_DE" + locale: "de_DE" # only used if babel python package is installed + dateFormatBabel: "full" # only used if babel python package is installed + # formatting options on https://babel.pocoo.org/en/latest/dates.html?highlight=name%20of%20day#date-fields timeFormat: "%H:%M" - dateFormat: "%A, %d. %B %Y" + dateFormat: "%A, %d. %B %Y" # ignored if babel python package is installed weatherEntity: weather.example pages: - type: cardEntities diff --git a/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py b/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py index bdf4f19a..8e1c028e 100644 --- a/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py +++ b/apps/nspanel-lovelace-ui/nspanel-lovelace-ui.py @@ -1,10 +1,16 @@ import json import datetime import hassapi as hass - import math import colorsys +# check Babel +import importlib +babel_spec = importlib.util.find_spec("babel") +if babel_spec is not None: + import babel.dates + + class NsPanelLovelaceUIManager(hass.Hass): def initialize(self): @@ -21,6 +27,8 @@ class NsPanelLovelaceUI: # check configured items self.check_items() + self.babel_spec = importlib.util.find_spec("babel") + # Setup, mqtt subscription and callback self.mqtt = self.api.get_plugin_api("MQTT") self.mqtt.mqtt_subscribe(topic=self.config["panelRecvTopic"]) @@ -141,9 +149,18 @@ class NsPanelLovelaceUI: self.send_mqtt_msg("time,{0}".format(time)) def update_date(self, kwargs): - # TODO: implement localization of date - date = datetime.datetime.now().strftime(self.config["dateFormat"]) - self.send_mqtt_msg("date,?{0}".format(date)) + if self.babel_spec is not None: + self.api.log("Babel package found", level="DEBUG") + if "dateFormatBabel" in self.config: + dateformat = self.config["dateFormatBabel"] + else: + dateformat = "full" + date = babel.dates.format_date(datetime.datetime.now(), dateformat, locale=self.config["locale"]) + self.send_mqtt_msg(f"date,?{date}") + else: + self.api.log("Babel package not found", level="DEBUG") + date = datetime.datetime.now().strftime(self.config["dateFormat"]) + self.send_mqtt_msg(f"date,?{date}") def update_screensaver_brightness(self, kwargs): self.current_screensaver_brightness = kwargs['value'] @@ -175,12 +192,12 @@ class NsPanelLovelaceUI: o1 = we.attributes.forecast[0]['datetime'] o1 = datetime.datetime.fromisoformat(o1) - o1 = o1.strftime("%a") + o1 = babel.dates.format_date(o1, "E", locale=self.config["locale"]) i1 = weathericons[we.attributes.forecast[0]['condition']] u1 = we.attributes.forecast[0]['temperature'] o2 = we.attributes.forecast[1]['datetime'] o2 = datetime.datetime.fromisoformat(o2) - o2 = o2.strftime("%a") + o2 = babel.dates.format_date(o2, "E", locale=self.config["locale"]) i2 = weathericons[we.attributes.forecast[1]['condition']] u2 = we.attributes.forecast[1]['temperature'] self.send_mqtt_msg(f"weatherUpdate,?{weathericons[we.state]}?{we.attributes.temperature}{unit}?{26}?{we.attributes.humidity} %?{o1}?{i1}?{u1}?{o2}?{i2}?{u2}") diff --git a/doc-pics/appdaemon-babel.png b/doc-pics/appdaemon-babel.png new file mode 100644 index 00000000..d8c43568 Binary files /dev/null and b/doc-pics/appdaemon-babel.png differ diff --git a/info.md b/info.md index 317e8bc6..31629e63 100644 --- a/info.md +++ b/info.md @@ -18,9 +18,11 @@ nspanel-1: value: 10 - time: "23:00:00" value: 0 - locale: "de_DE" + locale: "de_DE" # only used if babel python package is installed + dateFormatBabel: "full" # only used if babel python package is installed + # formatting options on https://babel.pocoo.org/en/latest/dates.html?highlight=name%20of%20day#date-fields timeFormat: "%H:%M" - dateFormat: "%A, %d. %B %Y" + dateFormat: "%A, %d. %B %Y" # ignored if babel python package is installed weatherEntity: weather.example pages: - type: cardEntities diff --git a/ioBroker/NsPanelTs.ts b/ioBroker/NsPanelTs.ts index 1438964e..3d2f6139 100644 --- a/ioBroker/NsPanelTs.ts +++ b/ioBroker/NsPanelTs.ts @@ -53,13 +53,12 @@ var config: Config = { pages: [ { "type": "cardEntities", - "heading": "Testseite", + "heading": "Haus", "items": [ "alias.0.Rolladen_Eltern", "alias.0.Erker", "alias.0.Küche", "alias.0.Wand" - ] }, { @@ -70,7 +69,6 @@ var config: Config = { "alias.0.Hausverbrauch", "alias.0.Pv", "alias.0.Batterie" - ] }, { @@ -92,8 +90,6 @@ on([config.pvEntity, config.batEntity], function () { HandleScreensaverUpdate(); }) - - on({ id: config.panelRecvTopic }, function (obj) { if (obj.state.val.startsWith('\{"CustomRecv":')) { var json = JSON.parse(obj.state.val); @@ -156,14 +152,14 @@ function HandleStartupProcess(): void { } function SendDate(): void { - var months = ["Jan", "Feb", "Mar", "Apr", "Mai", "Juni", "Juli", "Aug", "Sep", "Okt", "Nov", "Dez"]; + var months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]; var days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; var d = new Date(); var day = days[d.getDay()]; var date = d.getDate(); var month = months[d.getMonth()]; var year = d.getFullYear(); - var _sendDate = "date,?" + day + " " + date + "." + month + "." + year + ""; + var _sendDate = "date,?" + day + " " + date + " " + month + " " + year; SendToPanel({ payload: _sendDate }); } @@ -200,13 +196,6 @@ function CreateEntity(id: string, placeId: number): Payload { return { payload: "entityUpd," + placeId + "," + type }; } - // case "button": - // type = "button" - // iconId = 3 - // var optVal = "PRESS" - // out_msgs.push({ payload: "entityUpd," + (i + 1) + "," + type + "," + id + "," + iconId + "," + name + "," + optVal }) - // break - // ioBroker if (existsObject(id)) { let o = getObject(id) @@ -229,7 +218,7 @@ function CreateEntity(id: string, placeId: number): Payload { if (val === true || val === "true") optVal = "1" return { payload: "entityUpd," + placeId + "," + type + "," + id + "," + iconId + "," + name + "," + optVal } - break + case "dimmer": type = "light" iconId = 1 @@ -245,12 +234,12 @@ function CreateEntity(id: string, placeId: number): Payload { if (val === true || val === "true") optVal = "1" return { payload: "entityUpd," + placeId + "," + type + "," + id + "," + iconId + "," + name + "," + optVal } - break + case "blind": type = "shutter" iconId = 0 return { payload: "entityUpd," + placeId + "," + type + "," + id + "," + iconId + "," + name } - break + case "info": case "value.temperature": type = "text" @@ -266,14 +255,20 @@ function CreateEntity(id: string, placeId: number): Payload { } if (o.common.role == "value.temperature") { - iconId = 2 + iconId = 2; optVal += config.temperatureUnit; } else { optVal += GetUnitOfMeasurement(id + ".ACTUAL"); } - return { payload: "entityUpd," + placeId + "," + type + "," + id + "," + iconId + "," + name + "," + optVal } - break + return { payload: "entityUpd," + placeId + "," + type + "," + id + "," + iconId + "," + name + "," + optVal }; + + case "button": + type = "button"; + iconId = 3; + var optVal = "PRESS"; + return { payload: "entityUpd," + placeId + "," + type + "," + id + "," + iconId + "," + name + "," + optVal }; + default: break } @@ -332,7 +327,6 @@ function GenerateThermoPage(pageNum: number, page: PageThermo): Payload[] { if (existsState(id + ".SET")) destTemp = parseInt(getState(id + ".SET").val) * 10; - let status = "" if (existsState(id + ".MODE")) status = destTemp = getState(id + ".MODE").val; @@ -373,6 +367,8 @@ function HandleButtonEvent(words): void { setState(id + ".STOP", true) if (words[6] == "down") setState(id + ".CLOSE", true) + if (words[6] == "button") + setState(id + ".SET", true) if (words[6] == "positionSlider") setState(id + ".SET", parseInt(words[7])) @@ -399,17 +395,15 @@ function GenerateDetailPage(type: string, entityId: string): Payload[] { if (type == "popupLight") { let switchVal = "0" if (o.common.role == "light") { - if (existsState(id + ".GET")) - { + if (existsState(id + ".GET")) { val = getState(id + ".GET").val; RegisterDetailEntityWatcher(id + ".GET", id, type); } - else if (existsState(id + ".SET")) - { + else if (existsState(id + ".SET")) { val = getState(id + ".SET").val; RegisterDetailEntityWatcher(id + ".SET", id, type); } - + if (val) switchVal = "1" @@ -417,29 +411,23 @@ function GenerateDetailPage(type: string, entityId: string): Payload[] { } if (o.common.role == "dimmer") { - if (existsState(id + ".ON_ACTUAL")) - { + if (existsState(id + ".ON_ACTUAL")) { val = getState(id + ".ON_ACTUAL").val; RegisterDetailEntityWatcher(id + ".ON_ACTUAL", id, type); } - - else if (existsState(id + ".ON_SET")) - { + + else if (existsState(id + ".ON_SET")) { val = getState(id + ".ON_SET").val; RegisterDetailEntityWatcher(id + ".ON_SET", id, type); } - + if (val === true || val === "true") switchVal = "1" - let brightness = 0; - if (existsState(id + ".ACTUAL")) - { - brightness = Math.trunc(scale(getState(id + ".ACTUAL").val, 0, 100, 0, 100)) - RegisterDetailEntityWatcher(id + ".ACTUAL", id, type); - } - - - + let brightness = 0; + if (existsState(id + ".ACTUAL")) { + brightness = Math.trunc(scale(getState(id + ".ACTUAL").val, 0, 100, 0, 100)) + RegisterDetailEntityWatcher(id + ".ACTUAL", id, type); + } let colortemp = "disable" //let attr_support_color = attr.supported_color_modes //if (attr_support_color.includes("color_temp")) @@ -451,12 +439,10 @@ function GenerateDetailPage(type: string, entityId: string): Payload[] { } if (type == "popupShutter") { - if (existsState(id + ".ACTUAL")) val = getState(id + ".ACTUAL").val; else if (existsState(id + ".SET")) val = getState(id + ".SET").val; - out_msgs.push({ payload: "entityUpdateDetail," + val }) } } @@ -488,36 +474,82 @@ function HandleScreensaverUpdate(): void { let u1 = getState(config.batEntity).val; let u2 = getState(config.pvEntity).val; - SendToPanel({ payload: "weatherUpdate,?" + GetAccuWeatherIcon(parseInt(icon)) + "?" + temperature.toString() + " " + config.temperatureUnit + "?26?" + humidity + " %?Batterie?4?" + u1 + "%?PV?23?" + u2 + "W" }) + SendToPanel({ payload: "weatherUpdate,?" + GetAccuWeatherIcon(parseInt(icon)) + "?" + temperature.toString() + " " + config.temperatureUnit + "?26?" + humidity + " %?Batterie?34?" + u1 + "%?PV?32?" + u2 + "W" }) } } function GetAccuWeatherIcon(icon: number): number { switch (icon) { - case 6: // stark bewölkt - case 38: - return 12; - break; - case 1: // Sonnig - case 2: - case 3: - return 23; - case 12: // pouring - return 19; - case 18: // rainy - return 20; - case 32: // windig - return 24; - case 33: // klar - case 34: + case 24: // Ice + case 30: // Hot + case 31: // Cold + return 11; // exceptional + + case 7: // Cloudy + case 8: // Dreary (Overcast) + case 38: // Mostly Cloudy + return 12; // cloudy + + case 11: // fog + return 13; // fog + + case 25: // Sleet + return 14; // Hail + + case 15: // T-Storms + return 15; // lightning + + case 16: // Mostly Cloudy w/ T-Storms + case 17: // Partly Sunny w/ T-Storms + case 41: // Partly Cloudy w/ T-Storms + case 42: // Mostly Cloudy w/ T-Storms + return 16; // lightning-rainy + + case 33: // Clear + case 34: // Mostly Clear + case 37: // Hazy Moonlight return 17; - case 35: // partlycloudy - return 18; - case 11: // fog - return 13; + + case 3: // Partly Sunny + case 4: // Intermittent Clouds + case 6: // Mostly Cloudy + case 35: // Partly Cloudy + case 36: // Intermittent Clouds + return 18; // partlycloudy + + case 18: // pouring + return 19; // pouring + + case 12: // Showers + case 13: // Mostly Cloudy w/ Showers + case 14: // Partly Sunny w/ Showers + case 26: // Freezing Rain + case 39: // Partly Cloudy w/ Showers + case 40: // Mostly Cloudy w/ Showers + return 20; // rainy + + case 19: // Flurries + case 20: // Mostly Cloudy w/ Flurries + case 21: // Partly Sunny w/ Flurries + case 22: // Snow + case 23: // Mostly Cloudy w/ Snow + case 43: // Mostly Cloudy w/ Flurries + case 44: // Mostly Cloudy w/ Snow + return 21; // snowy + + case 29: // Rain and Snow + return 22; // snowy-rainy + + case 1: // Sunny + case 2: // Mostly Sunny + case 5: // Hazy Sunshine + return 23; // sunny + + case 32: // windy + return 24; // windy + default: return 1; - break; } }