Compare commits

...

77 Commits

Author SHA1 Message Date
Armilar
6e38d4f38d v4.3.3.24 - Update NsPanelTs.ts
* Log info commented out
2023-12-18 16:41:49 +01:00
Armilar
2344c9a9ed v4.3.3.24 - Update NsPanelTs.ts
* Hotfix Update Message
* Add Icon Colors to Entity Button
* Add Color-Const Cyan & Magenta
2023-12-18 16:37:46 +01:00
dependabot[bot]
5180f0f869 Bump home-assistant/builder from 2023.09.0 to 2023.12.0 (#1095)
Bumps [home-assistant/builder](https://github.com/home-assistant/builder) from 2023.09.0 to 2023.12.0.
- [Release notes](https://github.com/home-assistant/builder/releases)
- [Commits](https://github.com/home-assistant/builder/compare/2023.09.0...2023.12.0)

---
updated-dependencies:
- dependency-name: home-assistant/builder
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 11:11:52 +01:00
Armilar
b4f2789834 Merge pull request #1093 from joBr99/Armilar-patch-3
v4.3.3.23 - Update NsPanelTs.ts
2023-12-17 17:09:39 +01:00
Armilar
78c6029200 v4.3.3.23 - Update NsPanelTs.ts
Optimization of the blind control (enable or disable Up/Stop/Down)
2023-12-17 17:07:53 +01:00
Armilar
dca112e42b v4.3.3.23 - Update NsPanelTs.ts
Optimization of the blind control (enable or disable Up/Stop/Down)
2023-12-17 17:04:29 +01:00
Armilar
5ad16dd735 Update Countdown_Timer.ts 2023-12-16 10:42:51 +01:00
joBr99
723be0735e . 2023-12-15 14:29:31 +01:00
joBr99
a5bfb9388f . 2023-12-15 14:16:59 +01:00
joBr99
9c6f24f984 Merge branch 'main' of github.com:joBr99/nspanel-lovelace-ui 2023-12-15 14:09:52 +01:00
joBr99
59843ffea5 . 2023-12-15 14:09:25 +01:00
Armilar
4de9c4a12f 4.3.3.22 - Update ioBroker_NSPanel_locales_service.json
Update Notifications
2023-12-14 23:13:44 +01:00
Armilar
3eb05e5a84 4.3.3.22 - Update NsPanelTs.ts 2023-12-14 23:08:14 +01:00
Armilar
5d7a7ed1a4 Merge pull request #1090 from tt-tom17/main
v4.3.3.22 Update NSPanel.ts
2023-12-14 22:44:30 +01:00
Thomas
7124a22c38 v4.3.3.22 Update NSPanel.ts
- Add UpdateMessage => disable the update messages
- Fix name by static Navi Icon
- Fix colorscale by Role Info
2023-12-14 22:36:41 +01:00
Armilar
fa4d65a383 Merge pull request #1089 from tt-tom17/main
Update Alarm_clock.ts
2023-12-13 22:01:03 +01:00
Thomas
d26306f892 Update Alarm_clock.ts
fix dpAction to val: true
2023-12-13 21:55:33 +01:00
joBr99
9c0bb037fb Merge branch 'main' of github.com:joBr99/nspanel-lovelace-ui 2023-12-13 16:50:46 +01:00
joBr99
4b73e20b9b implement templtates for brightness 2023-12-13 16:50:28 +01:00
dependabot[bot]
3940a0c2e9 Bump actions/setup-python from 4 to 5 (#1087)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-13 14:22:56 +01:00
Armilar
8f57d4a642 v4.3.3.21 - Update NsPanelTs.ts
- Add createAutoALias to popupTimer only for Time
2023-12-09 11:19:38 +01:00
Armilar
3979fdf6a0 Create Countdown_Timer.ts 2023-12-08 22:58:24 +01:00
Armilar
4cd47126eb Create Alarm_clock.ts 2023-12-08 22:54:16 +01:00
Armilar
eb0dd79c80 v4.3.3.20 - Update ioBroker_NSPanel_locales.json 2023-12-08 22:41:11 +01:00
Armilar
3a39a8ca0e Merge pull request #1086 from tt-tom17/main
v4.3.3.20 - Update NSPanel.ts
2023-12-08 22:27:04 +01:00
Armilar
b5c4a2128b v4.3.3.20 - Update NsPanelTs.ts
Remove semicolon
2023-12-08 22:25:04 +01:00
Armilar
41f43fe5d0 v4.3.3.20 - Update NsPanelTs.ts
Remove Example
2023-12-08 22:22:44 +01:00
Armilar
f3f93b7136 v4.3.3.12 - Update NsPanelTs.ts 2023-12-08 22:20:08 +01:00
joBr99
2fb1855842 fixed binary sensors 2023-12-08 22:09:35 +01:00
Thomas
9a3627427f v4.3.3.20 - Update NSPanel.ts
add Role AlarmTime for Alarm Clock
2023-12-08 20:24:23 +01:00
Armilar
94fbf0a5f7 Merge pull request #1084 from joBr99/Armilar-patch-2
v4.3.3.19 - Update NsPanelTs.ts
2023-12-07 20:02:54 +01:00
Armilar
caddec1190 v4.3.3.19 - Update NsPanelTs.ts
- Fix Trigger activeDimmodeBrightness if Dimmode = -1
2023-12-07 20:02:12 +01:00
Armilar
03367ea27d v4.3.3.19 - Update NsPanelTs.ts
- Fix Trigger activeDimmodeBrightness if Dimmode = -1
2023-12-07 19:58:21 +01:00
Armilar
8e792ae8fc Merge pull request #1083 from joBr99/Armilar-patch-1
v4.3.3.18 - Update NsPanelTs.ts
2023-12-06 22:00:28 +01:00
Armilar
0a03f736ce v4.3.3.18 - Update NsPanelTs.ts
- Add (ELAPSED/DURATION) to v2Adapter alexa2
- Replace missing Type console.log --> log(message, 'serverity')
2023-12-06 21:49:15 +01:00
Armilar
8d7f6ffea5 v4.3.3.18 - Update NsPanelTs.ts
- Add (ELAPSED/DURATION) to v2Adapter alexa2
- Replace missing Type console.log --> log(message, 'serverity')
2023-12-06 15:20:28 +01:00
Armilar
93a6a7a88a v4.3.3.17 - Update NsPanelTs.ts
Add SEEK and CROSSFADE to Sonos cardMedia
2023-12-04 23:01:01 +01:00
Armilar
3913b17596 Update ioBroker_NSPanel_locales.json 2023-12-04 22:06:42 +01:00
Armilar
d08e8eb40c Update ioBroker_NSPanel_locales.json 2023-12-04 21:57:44 +01:00
Johannes
c2d281658e Update panel_cmd.py 2023-12-03 16:25:17 +01:00
joBr99
108582cbfb fix 2023-12-02 16:27:50 +01:00
joBr99
b17db265f4 implement temp unit 2023-12-02 16:26:28 +01:00
joBr99
206739dcc5 implement popup on card thermo 2023-12-02 16:20:31 +01:00
joBr99
adcb618a11 . 2023-12-02 14:23:43 +01:00
joBr99
dfba3b6e84 Merge branch 'main' of github.com:joBr99/nspanel-lovelace-ui 2023-12-02 14:11:25 +01:00
joBr99
2726859135 implement input_select and select for light effects 2023-12-02 14:10:53 +01:00
Armilar
c0e20e6f25 Merge pull request #1077 from joBr99/Armilar-patch-1
v4.3.3.16 - Update NsPanelTs.ts
2023-12-02 13:02:38 +01:00
Armilar
148c2fc5a2 v4.3.3.16 - Update NsPanelTs.ts
- Beautification of the Sonos player Strings / Add Duration & Elapsed
- Fix Datapoints with Value null with -1
- Request replaced by Axios
2023-12-02 00:50:11 +01:00
Armilar
526f5e8946 v4.3.3.16 - Update NsPanelTs.ts
- Beautification of the Sonos player Strings / Add Duration & Elapsed
- Fix Datapoints with Value null with -1
- Request replaced by Axios
2023-12-02 00:36:00 +01:00
joBr99
8cd17b9d9a . 2023-12-02 00:03:22 +01:00
joBr99
70ff46ab4b . 2023-12-01 23:59:59 +01:00
joBr99
770348b07b . 2023-12-01 23:48:11 +01:00
joBr99
4795cc23ad fix state update bug with iid 2023-12-01 23:39:49 +01:00
joBr99
bae64dcee5 do not init mqtt in case ha api is used 2023-12-01 23:28:58 +01:00
joBr99
953a8d7110 . 2023-12-01 23:26:52 +01:00
joBr99
3b5eaac976 initial implementation of esphome api comm 2023-12-01 23:26:27 +01:00
joBr99
6e28237ec5 Merge branch 'main' of github.com:joBr99/nspanel-lovelace-ui 2023-12-01 19:55:36 +01:00
joBr99
b8c47948c3 add queue for outgoing messages 2023-12-01 19:55:29 +01:00
Armilar
0e8f9ad220 Merge pull request #1075 from tt-tom17/main
v4.3.3.15 - Update NsPanelTs.ts
2023-12-01 13:11:13 +01:00
Thomas
79e43e2740 Update NsPanelTs.ts
fix activeDimmodeBrightness -> value -1
fix bExitPage -> value -1
2023-12-01 11:22:01 +01:00
joBr99
c32d2958a6 . 2023-11-30 17:48:31 +01:00
joBr99
3b7c934972 . 2023-11-30 17:43:53 +01:00
joBr99
40f29d09c1 . 2023-11-30 17:39:04 +01:00
joBr99
b601f2d860 . 2023-11-30 17:32:21 +01:00
joBr99
5953d7c8dd . 2023-11-30 17:25:32 +01:00
joBr99
e9859c0d32 . 2023-11-30 17:20:12 +01:00
joBr99
4ad997515f Merge branch 'main' of github.com:joBr99/nspanel-lovelace-ui 2023-11-30 17:15:03 +01:00
joBr99
ba637cf11e . 2023-11-30 17:14:41 +01:00
Armilar
a4abcd1734 Merge pull request #1074 from joBr99/Armilar-patch-1
v4.3.3.15 - Minor bug fixes
2023-11-30 00:06:06 +01:00
Armilar
86bbd36813 v4.3.3.15 - Regex Tracklist
- Regex Tracklist
2023-11-30 00:04:52 +01:00
Armilar
c9dffc431c v4.3.3.15 - Minor bug fixes
- Fix cardMedia Volume-Slider
- Add Init Release to Startup
2023-11-29 21:24:02 +01:00
joBr99
04ffd6257e . 2023-11-29 17:25:25 +01:00
Johannes
4102f56cee Update docs-release.yml 2023-11-28 23:46:03 +01:00
Johannes
6a62a6206a Update docs-release.yml 2023-11-28 23:44:30 +01:00
Johannes
0bddceccfa Update docs-release.yml 2023-11-28 23:44:16 +01:00
Johannes
09156fbc89 Update docs-release.yml 2023-11-28 23:43:18 +01:00
joBr99
76d0075c7d . 2023-11-28 23:41:28 +01:00
16 changed files with 1919 additions and 1038 deletions

View File

@@ -100,7 +100,7 @@ jobs:
- name: Build ${{ matrix.addon }} add-on
if: steps.check.outputs.build_arch == 'true'
uses: home-assistant/builder@2023.09.0
uses: home-assistant/builder@2023.12.0
with:
args: |
${{ env.BUILD_ARGS }} \

View File

@@ -1,10 +1,10 @@
name: docs-ci
name: docs-ci
on:
workflow_dispatch:
push:
branches:
- main
branches:
- dev
paths:
- docs/*
- .github/workflows/docs.yml
@@ -18,18 +18,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-video markdown-include mike
- run: cp HMI/README.md docs/hmi-serial-protocol.md
#- run: mkdocs gh-deploy --force
- run: git config --global user.name Docs deploy
- run: git config --global user.email docs@dummy.bot.com
- run: mike deploy --push --update-aliases dev

29
.github/workflows/docs-release.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: docs-ci
on:
workflow_dispatch:
push:
branches:
- main
paths:
- docs/*
- .github/workflows/docs-release.yml
- mkdocs.yml
- HMI/README.md
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-video markdown-include mike
- run: cp HMI/README.md docs/hmi-serial-protocol.md
- run: git config --global user.name Docs deploy
- run: git config --global user.email docs@dummy.bot.com
- run: mike set-default stable
- run: mike deploy --push --update-aliases stable

View File

@@ -0,0 +1,44 @@
const dp_userdata: string = '0_userdata.0.NSPanel';
const dp_alias: string = 'alias.0.NSPanel';
// dpAction wird wenn der Wecker gestellt wird auf false geschaltet
// dpAction wird wenn die Weckzeit erreicht ist auf true geschaltet
// Der nachfolgende Datenpunkt muss manuell erstellt werden...
const dpAction: string = '0_userdata.0.example_boolean';
const Debug = true;
let time: number;
let scheduleAlarmTime: any = null;
on({ id: dp_userdata + '.AlarmTime.State', change: 'ne' }, async (obj) => {
time = getState(dp_userdata + '.AlarmTime.Time').val;
if (Debug) log('Uhrzeit: ' + time, 'info');
if ('paused' == obj.state.val) {
(function () { if (scheduleAlarmTime) {
clearSchedule(scheduleAlarmTime);
scheduleAlarmTime = null;
}
});
} else if ('active' == obj.state.val) {
let stunde: number = Math.floor(time / 60);
let minute: number = time % 60;
if (Debug) log('Weckzeit: ' + ('0' + stunde).slice(-2) + ':' + ('0' + minute).slice(-2), 'info');
scheduleAlarmTime = schedule(minute + ' ' + stunde + ' * * *', async () => {
await setStateAsync(dpAction, <iobJS.State>{ val: true, ack: true });
await setStateAsync(dp_userdata + '.AlarmTime.State', <iobJS.State>{ val: 'paused', ack: true });
});
}
});
async function Init_Datenpunkte() {
if (existsState(dp_alias + '.AlarmTime.ACTUAL') == false) {
await createStateAsync(dp_userdata + '.AlarmTime.Time', '0', { type: 'number' });
await createStateAsync(dp_userdata + '.AlarmTime.State', 'paused', { type: 'string' });
setObject(dp_alias + '.AlarmTime', { type: 'channel', common: { role: 'value.alarmtime', name: 'Alarmtime' }, native: {} });
await createAliasAsync(dp_alias + '.AlarmTime.ACTUAL', dp_userdata + '.AlarmTime.Time', true, <iobJS.StateCommon>{ type: 'number', role: 'state', name: 'ACTUAL' });
await createAliasAsync(dp_alias + '.AlarmTime.STATE', dp_userdata + '.AlarmTime.State', true, <iobJS.StateCommon>{ type: 'string', role: 'state', name: 'STATE' });
log("<PageItem>{id: '"+ dp_alias + ".AlarmTime', name: 'Wecker', onColor: Red, offColor: Green, useColor: true}", 'info');
}
}
Init_Datenpunkte();

View File

@@ -0,0 +1,46 @@
const dp_userdata: string = '0_userdata.0.NSPanel';
const dp_alias: string = 'alias.0.NSPanel';
// Der nachfolgende Datenpunkt muss manuell angelegt werden
const dpAction: string = '0_userdata.0.example_boolean'; // anpassen
const Debug = false;
let intervallCounter: any;
let sec_timer = getState(dp_userdata + '.Countdown.Time').val;
on({ id: dp_userdata + '.Countdown.State', change: 'ne' }, async (obj) => {
switch (obj.state.val) {
case 'active':
if (intervallCounter) { clearInterval(intervallCounter); intervallCounter = null; };
intervallCounter = setInterval(async () => {
if (getState(dp_userdata + '.Countdown.Time').val > 0) {
sec_timer = getState(dp_userdata + '.Countdown.Time').val;
setState(dp_userdata + '.Countdown.Time', (sec_timer - 1), false);
} else {
setState(dp_userdata + '.Countdown.Time', 0, false);
setState(dp_userdata + '.Countdown.State', 'idle', false);
// An dieser Stelle kann auch noch eine Meldung an Alexa oder Telegram, etc. erfolgen
}
}, 1000);
break;
default:
if (intervallCounter) { clearInterval(intervallCounter); intervallCounter = null; };
break;
}
});
async function Init_Datenpunkte() {
if (existsState(dp_alias + '.Countdown.ACTUAL') == false) {
await createStateAsync(dp_userdata + '.Countdown.Time', '0', { type: 'number'});
await createStateAsync(dp_userdata + '.Countdown.State', 'paused', { type: 'string' });
setObject(dp_alias + '.Countdown', { type: 'channel', common: { role: 'level.timer', name: 'Countdown' }, native: {} });
await createAliasAsync(dp_alias + '.Countdown.ACTUAL', dp_userdata + '.Countdown.Time', true, <iobJS.StateCommon>{ type: 'number', role: 'state', name: 'ACTUAL' });
await createAliasAsync(dp_alias + '.Countdown.STATE', dp_userdata + '.Countdown.State', true, <iobJS.StateCommon>{ type: 'string', role: 'state', name: 'STATE' });
log("<PageItem>{id: '"+ dp_alias + ".Countdown', name: 'Timer'}", 'info');
}
}
Init_Datenpunkte();

File diff suppressed because it is too large Load Diff

View File

@@ -1047,6 +1047,14 @@
"zh-CN":"空闲",
"zh-TW":"暫停"
},
"on":{
"en-US":"On",
"de-DE":"Ein"
},
"off":{
"en-US":"Off",
"de-DE":"Aus"
},
"paused":{
"en-US":"Paused",
"de-DE":"pausiert",
@@ -2533,6 +2541,22 @@
"zh-CN":"没有音乐可以控制",
"zh-TW":"沒有音樂可以控制"
},
"on":{
"en-US":"On",
"de-DE":"An"
},
"off":{
"en-US":"Off",
"de-DE":"Aus"
},
"seek":{
"en-US":"Seek",
"de-DE":"Suchen"
},
"crossfade":{
"en-US":"Crossfade",
"de-DE":"Überblenden"
},
"speaker":{
"en-US":"Speakerlist",
"de-DE":"Wiedergabegeräte",

View File

@@ -2693,5 +2693,9 @@
"update_nextion_tft":{
"en-US":"Update Nextion TFT",
"de-DE":"Nextion TFT Update"
},
"update_message":{
"en-US":"Update Notifications",
"de-DE":"Update Mitteilungen"
}
}

View File

@@ -1,6 +1,6 @@
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
name: NSPanel Lovelace UI Addon
version: "4.7.54"
version: "4.7.73"
slug: nspanel-lovelace-ui
description: NSPanel Lovelace UI Addon
services:

View File

@@ -52,6 +52,8 @@ class HAEntity(panel_cards.Entity):
self.state = data.get("state")
self.attributes = data.get("attributes", [])
else:
self.state = "not found"
self.attributes = []
return "~text~iid.404~X~6666~not found~"
# HA Entities
@@ -188,7 +190,7 @@ class HAEntity(panel_cards.Entity):
icon_char = value
case 'binary_sensor':
device_class = self.attributes.get("device_class", "")
value = get_translation(self.locale, f"backend.component.binary_sensor.state.{device_class}.{entity.state}")
value = get_translation(self.locale, f"backend.component.binary_sensor.state.{device_class}.{self.state}")
case 'weather':
attr = self.config.get("attribute", "temperature")
value = str(self.attributes.get(attr, self.state))
@@ -297,21 +299,19 @@ class EntitiesCard(HACard):
result = f"{self.title}~{self.gen_nav()}"
for e in self.entities:
result += e.render(cardType=self.type)
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class QRCard(HACard):
def __init__(self, locale, config, panel):
super().__init__(locale, config, panel)
self.qrcode = config.get("qrCode", "https://www.youtube.com/watch?v=dQw4w9WgXcQ")
def render(self):
# TODO: Render QRCode as HomeAssistant Template
#qrcode = apis.ha_api.render_template(qrcode)
if self.qrcode.startswith("ha:"):
self.qrcode = libs.home_assistant.get_template(self.qrcode)[3:]
result = f"{self.title}~{self.gen_nav()}~{self.qrcode}"
for e in self.entities:
result += e.render()
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class PowerCard(HACard):
def __init__(self, locale, config, panel):
@@ -329,7 +329,7 @@ class PowerCard(HACard):
# if isinstance(speed, str):
# speed = apis.ha_api.render_template(speed)
result += f"~{speed}"
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class MediaCard(HACard):
def __init__(self, locale, config, panel):
@@ -366,7 +366,7 @@ class MediaCard(HACard):
for e in self.entities[1:]:
button_str += e.render()
result = f"{self.title}~{self.gen_nav()}~{main_entity.entity_id}~{title}~~{author}~~{volume}~{iconplaypause}~{onoffbutton}~{shuffleBtn}{media_icon}{button_str}"
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class ClimateCard(HACard):
def __init__(self, locale, config, panel):
@@ -375,9 +375,8 @@ class ClimateCard(HACard):
def render(self):
main_entity = self.entities[0]
#TODO: temp unit
temp_unit = "celsius"
if(temp_unit == "celsius"):
temp_unit = self.panel.temp_unit
if temp_unit == "celsius":
temperature_unit_icon = get_icon_char("temperature-celsius")
temperature_unit = "°C"
@@ -411,7 +410,12 @@ class ClimateCard(HACard):
min_temp = int(main_entity.attributes.get("min_temp", 0)*10)
max_temp = int(main_entity.attributes.get("max_temp", 0)*10)
step_temp = int(main_entity.attributes.get("target_temp_step", 0.5)*10)
if temp_unit == "celsius":
step_default = 0.5
else:
step_default = 0.5
step_temp = int(main_entity.attributes.get("target_temp_step", step_default)*10)
icon_res_list = []
icon_res = ""
@@ -460,8 +464,8 @@ class ClimateCard(HACard):
if any(x in ["preset_modes", "swing_modes", "fan_modes"] for x in main_entity.attributes):
detailPage = "0"
result = f"{self.title}~{self.gen_nav()}~{main_entity.entity_id}~{current_temp} {temperature_unit}~{dest_temp}~{state_value}~{min_temp}~{max_temp}~{step_temp}{icon_res}~{currently_translation}~{state_translation}~{action_translation}~{temperature_unit_icon}~{dest_temp2}~{detailPage}"
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
result = f"{self.title}~{self.gen_nav()}~iid.{main_entity.iid}~{current_temp} {temperature_unit}~{dest_temp}~{state_value}~{min_temp}~{max_temp}~{step_temp}{icon_res}~{currently_translation}~{state_translation}~{action_translation}~{temperature_unit_icon}~{dest_temp2}~{detailPage}"
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class AlarmCard(HACard):
def __init__(self, locale, config, panel):
@@ -471,6 +475,8 @@ class AlarmCard(HACard):
main_entity = self.entities[0]
main_entity.render()
print(main_entity.state)
icon = get_icon_char("shield-off")
color = rgb_dec565([255,255,255])
supported_modes = []
@@ -531,7 +537,7 @@ class AlarmCard(HACard):
if len(supported_modes) < 4:
arm_buttons += "~"*((4-len(supported_modes))*2)
result = f"{self.title}~{self.gen_nav()}~{main_entity.entity_id}{arm_buttons}~{icon}~{color}~{numpad}~{flashing}~{add_btn}"
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class UnlockCard(HACard):
@@ -544,7 +550,7 @@ class UnlockCard(HACard):
icon = get_icon_char("lock")
supported_modes = ["cardUnlock-unlock"]
entity_id = self.config.get("entity")
entity_id = self.config.get("destination")
# add padding to arm buttons
arm_buttons = ""
@@ -555,7 +561,7 @@ class UnlockCard(HACard):
numpad = "enable"
result = f"{self.title}~{self.gen_nav()}~{entity_id}{arm_buttons}~{icon}~{color}~{numpad}~disable~"
libs.panel_cmd.entityUpd(self.panel.sendTopic, result)
libs.panel_cmd.entityUpd(self.panel.msg_out_queue, self.panel.sendTopic, result)
class Screensaver(HACard):
def __init__(self, locale, config, panel):
@@ -583,7 +589,7 @@ class Screensaver(HACard):
result = ""
for e in self.entities:
result += e.render(cardType=self.type)
libs.panel_cmd.weatherUpdate(self.panel.sendTopic, result[1:])
libs.panel_cmd.weatherUpdate(self.panel.msg_out_queue, self.panel.sendTopic, result[1:])
statusUpdateResult = ""
icon1font = ""
@@ -601,7 +607,7 @@ class Screensaver(HACard):
else:
statusUpdateResult += "~~"
libs.panel_cmd.statusUpdate(self.panel.sendTopic, f"{statusUpdateResult}~{icon1font}~{icon2font}")
libs.panel_cmd.statusUpdate(self.panel.msg_out_queue, self.panel.sendTopic, f"{statusUpdateResult}~{icon1font}~{icon2font}")
def card_factory(locale, settings, panel):
@@ -625,7 +631,7 @@ def card_factory(locale, settings, panel):
return "NotImplemented", None
return card.iid, card
def detail_open(locale, detail_type, ha_entity_id, entity_id, sendTopic=None):
def detail_open(locale, detail_type, ha_entity_id, entity_id, msg_out_queue, sendTopic=None, options_list=None):
data = libs.home_assistant.get_entity_data(ha_entity_id)
if data:
state = data.get("state")
@@ -762,9 +768,51 @@ def detail_open(locale, detail_type, ha_entity_id, entity_id, sendTopic=None):
return f'{entity_id}~~{icon_color}~{switch_val}~{speed}~{speedMax}~{speed_translation}~{preset_mode}~{preset_modes}'
case 'popupThermo' | 'climate':
print(f"not implemented {detail_type}")
case 'popupInSel' | 'input_select' | 'select':
print(f"not implemented {detail_type}")
icon_id = ha_icons.get_icon_ha("climate", state)
icon_color = ha_colors.get_entity_color("climate", state, attributes)
modes_out = ""
for mode in ["preset_modes", "swing_modes", "fan_modes"]:
heading = get_translation(locale, f"frontend.ui.card.climate.{mode[:-1]}")
cur_mode = attributes.get(mode[:-1], "")
modes = attributes.get(mode, [])
if modes is not None:
if mode == "preset_modes":
translated_modes = []
for elem in modes:
translated_modes.append(get_translation(locale, f"frontend.state_attributes.climate.preset_mode.{elem}"))
cur_mode = get_translation(locale, f"frontend.state_attributes.climate.preset_mode.{cur_mode}")
modes_res = "?".join(translated_modes)
else:
modes_res = "?".join(modes)
if modes:
modes_out += f"{heading}~{mode}~{cur_mode}~{modes_res}~"
return f"{entity_id}~{icon_id}~{icon_color}~{modes_out}"
case 'popupInSel' | 'input_select' | 'select' | 'media_player':
hatype = ha_entity_id.split(".")[0]
options = []
icon_color = 0
icon_color = ha_colors.get_entity_color(detail_type, state, attributes)
state = state
if hatype in ["input_select", "select"]:
options = attributes.get("options", [])
elif hatype == "light":
if options_list is not None:
options = options_list
else:
options = attributes.get("effect_list", [])[:15]
state = attributes.get("effect")
elif hatype == "media_player":
state = attributes.get("source", "")
options = attributes.get("source_list", [])
options = "?".join(options)
return f"{entity_id}~~{icon_color}~{hatype}~{state}~{options}~"
case 'popupTimer' | 'timer':
icon_color = ha_colors.get_entity_color("timer", state, attributes)
if state in ["idle", "paused"]:
@@ -787,8 +835,8 @@ def detail_open(locale, detail_type, ha_entity_id, entity_id, sendTopic=None):
#update timer in a second
def update_time():
time.sleep(1)
out = detail_open(locale, detail_type, ha_entity_id, entity_id, sendTopic=sendTopic)
libs.panel_cmd.entityUpdateDetail(sendTopic, out)
out = detail_open(locale, detail_type, ha_entity_id, entity_id, msg_out_queue, sendTopic=sendTopic)
libs.panel_cmd.entityUpdateDetail(msg_out_queue, sendTopic, out)
tt = threading.Thread(target=update_time, args=())
tt.daemon = True
tt.start()

View File

@@ -8,6 +8,7 @@ def wait_for_ha_cache():
while time.time() < mustend:
if len(libs.home_assistant.home_assistant_entity_state_cache) == 0:
time.sleep(0.1)
time.sleep(1)
def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, screenBrightness, sleepOverride, return_involved_entities=False):
dimmode = 10
@@ -17,6 +18,12 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
if sleepBrightness:
if isinstance(sleepBrightness, int):
dimmode = sleepBrightness
elif isinstance(sleepBrightness, list):
logging.error("list style config for sleepBrightness no longer supported")
elif sleepBrightness.startswith("ha:"):
time.sleep(1)
dimmode = int(float(libs.home_assistant.get_template(sleepBrightness)[3:]))
involved_entities.extend(libs.home_assistant.get_template_listener_entities(sleepBrightness))
elif libs.home_assistant.is_existent(sleepBrightness):
involved_entities.append(sleepBrightness)
dimmode = int(float(libs.home_assistant.get_entity_data(sleepBrightness).get('state', 10)))
@@ -24,6 +31,12 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
if screenBrightness:
if isinstance(screenBrightness, int):
dimValueNormal = screenBrightness
elif isinstance(screenBrightness, list):
logging.error("list style config for screenBrightness no longer supported")
elif screenBrightness.startswith("ha:"):
time.sleep(1)
dimValueNormal = int(float(libs.home_assistant.get_template(screenBrightness)[3:]))
involved_entities.extend(libs.home_assistant.get_template_listener_entities(screenBrightness))
elif libs.home_assistant.is_existent(screenBrightness):
involved_entities.append(screenBrightness)
dimValueNormal = int(float(libs.home_assistant.get_entity_data(screenBrightness).get('state', 100)))
@@ -51,7 +64,7 @@ def calculate_dim_values(sleepTracking, sleepTrackingZones, sleepBrightness, scr
else:
return dimmode, dimValueNormal
def handle_buttons(entity_id, btype, value):
def handle_buttons(entity_id, btype, value, entity_config=None):
match btype:
case 'button':
button_press(entity_id, value)
@@ -60,7 +73,7 @@ def handle_buttons(entity_id, btype, value):
case 'number-set':
if entity_id.startswith('fan'):
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
value = float(value) * float(attr.get(percentage_step, 0))
value = float(value) * float(attr.get('percentage_step', 0))
service_data = {
"value": int(value)
}
@@ -156,19 +169,20 @@ def handle_buttons(entity_id, btype, value):
}
call_ha_service(entity_id, f"alarm_{btype}", service_data=service_data)
case 'mode-preset_modes' | 'mode-swing_modes' | 'mode-fan_modes':
attr = libs.home_assistant.get_entity_data(entity_id).get('attributes', [])
mapping = {
'mode-preset_modes': 'preset_modes',
'mode-swing_modes': 'swing_modes',
'mode-fan_modes': 'fan_mode'
'mode-fan_modes': 'fan_modes'
}
if btype in mapping:
modes = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get(mapping[btype], [])
modes = attr.get(mapping[btype], [])
if modes:
mode = modes[int(value)]
service_data = {
mapping[btype]: mode
mapping[btype][:-1]: mode
}
call_ha_service(entity_id, f"set_{mapping[btype]}", service_data=service_data)
call_ha_service(entity_id, f"set_{mapping[btype][:-1]}", service_data=service_data)
case 'mode-input_select' | 'mode-select':
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("options", [])
if options:
@@ -186,7 +200,7 @@ def handle_buttons(entity_id, btype, value):
}
call_ha_service(entity_id, "select_source", service_data=service_data)
case 'mode-light':
options = libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("effect_list", [])
options = entity_config.get("effectList", libs.home_assistant.get_entity_data(entity_id).get('attributes', []).get("effect_list", []))
if options:
option = options[int(value)]
service_data = {
@@ -216,30 +230,6 @@ def handle_buttons(entity_id, btype, value):
case _:
logging.error("Not implemented: %s", btype)
# # for cardUnlock
# if button_type == "cardUnlock-unlock":
# curCard = self._config.get_card_by_uuid(
# entity_id.replace('navigate.', ''))
# if curCard is not None:
# if int(curCard.raw_config.get("pin")) == int(value):
# dstCard = self._config.search_card(
# curCard.raw_config.get("destination"))
# if dstCard is not None:
# if dstCard.hidden:
# self._previous_cards.append(self._current_card)
# self._current_card = dstCard
# self._pages_gen.render_card(self._current_card)
# if button_type == "opnSensorNotify":
# msg = ""
# entity = apis.ha_api.get_entity(entity_id)
# if "open_sensors" in entity.attributes and entity.attributes.open_sensors is not None:
# for e in entity.attributes.open_sensors:
# msg += f"- {apis.ha_api.get_entity(e).attributes.friendly_name}\r\n"
# self._pages_gen.send_message_page(
# "opnSensorNotifyRes", "", msg, "", "")
def call_ha_service(entity_id, service, service_data = {}):
etype = entity_id.split(".")[0]
libs.home_assistant.call_service(

View File

@@ -70,7 +70,8 @@ def on_message(ws, message):
for template, template_cache_entry in template_cache.items():
if entity_id in template_cache_entry.get("listener-entities", []):
cache_template(template)
elif json_msg["type"] == "event" and json_msg["event"]["event_type"] == "esphome.nspanel.data":
nspanel_data_callback(json_msg["event"]["data"]["device_id"], json_msg["event"]["data"]["CustomRecv"])
elif json_msg["type"] == "result" and not json_msg["success"]:
logging.error("Failed result: ")
logging.error(json_msg)
@@ -143,6 +144,15 @@ def subscribe_to_events():
}
send_message(json.dumps(msg))
def subscribe_to_nspanel_events(nsp_callback):
global next_id, nspanel_data_callback
nspanel_data_callback = nsp_callback
msg = {
"id": next_id,
"type": "subscribe_events",
"event_type": "esphome.nspanel.data"
}
send_message(json.dumps(msg))
def _get_all_states():
global next_id, request_all_states_id
@@ -158,6 +168,10 @@ def send_entity_update(entity_id):
global on_ha_update
on_ha_update(entity_id)
def nspanel_data_callback(device_id, msg):
global nspanel_data_callback
nspanel_data_callback(device_id, msg)
def call_service(entity_name: str, domain: str, service: str, service_data: dict) -> bool:
global next_id
try:
@@ -177,6 +191,22 @@ def call_service(entity_name: str, domain: str, service: str, service_data: dict
logging.exception("Failed to call Home Assisatant service.")
return False
def send_msg_to_panel(service: str, service_data: dict) -> bool:
global next_id
try:
msg = {
"id": next_id,
"type": "call_service",
"domain": "esphome",
"service": service,
"service_data": service_data,
}
send_message(json.dumps(msg))
return True
except Exception as e:
logging.exception("Failed to call Home Assisatant service.")
return False
def execute_script(entity_name: str, domain: str, service: str, service_data: dict) -> str:
global next_id, response_buffer
try:
@@ -202,7 +232,7 @@ def execute_script(entity_name: str, domain: str, service: str, service_data: di
]
}
send_message(json.dumps(msg))
# busy waiting for response with a timeout of 0.2 seconds - maybe there's a better way of doing this
# busy waiting for response with a timeout of 0.4 seconds- maybe there's a better way of doing this
mustend = time.time() + 0.4
while time.time() < mustend:
if response_buffer[call_id] == True:
@@ -216,6 +246,8 @@ def execute_script(entity_name: str, domain: str, service: str, service_data: di
return {}
def cache_template(template):
if not template:
raise Exception("Invalid template")
global next_id, response_buffer
try:
call_id = next_id
@@ -243,6 +275,18 @@ def get_template(template):
else:
return template_cache.get(template, []).get("result", "404")
def get_template_listener_entities(template):
global template_cache
if template in template_cache:
return template_cache[template].get("listener-entities")
else:
mustend = time.time() + 0.5
while time.time() < mustend:
if template not in template_cache:
time.sleep(0.0001)
else:
return template_cache.get(template, []).get("listener-entities", "404")
def get_entity_data(entity_id: str):
if entity_id in home_assistant_entity_state_cache:
return home_assistant_entity_state_cache[entity_id]

View File

@@ -1,46 +1,47 @@
import logging
def init(mqtt_client_from_manager):
global mqtt_client
mqtt_client = mqtt_client_from_manager
def custom_send(topic, msg):
global mqtt_client
mqtt_client.publish(topic, msg)
logging.debug("Sent Message to NsPanel (%s): %s", topic, msg)
def page_type(topic, target_page):
if target_page == "cardUnlock":
target_page = "cardAlarm"
custom_send(topic, f"pageType~{target_page}")
def send_time(topic, time, addTimeText=""):
custom_send(topic, f"time~{time}~{addTimeText}")
def send_date(topic, date):
custom_send(topic, f"date~{date}")
def entityUpd(topic, data):
custom_send(topic, f"entityUpd~{data}")
def weatherUpdate(topic, data):
custom_send(topic, f"weatherUpdate~{data}")
def timeout(topic, timeout):
custom_send(topic, f"timeout~{timeout}")
def dimmode(topic, dimValue, dimValueNormal, backgroundColor, fontColor, featExperimentalSliders):
if dimValue==dimValueNormal:
dimValue=dimValue-1
custom_send(topic, f"dimmode~{dimValue}~{dimValueNormal}~{backgroundColor}~{fontColor}~{featExperimentalSliders}")
def entityUpdateDetail(topic, data):
custom_send(topic, f"entityUpdateDetail~{data}")
def statusUpdate(topic, data):
custom_send(topic, f"statusUpdate~{data}")
import logging
def custom_send(msg_out_queue, topic, msg):
msg_out_queue.put((topic, msg))
logging.debug("Sent Message to NsPanel (%s): %s", topic, msg)
def page_type(msg_out_queue, topic, target_page):
if target_page == "cardUnlock":
target_page = "cardAlarm"
custom_send(msg_out_queue, topic, f"pageType~{target_page}")
def send_time(msg_out_queue, topic, time, addTimeText=""):
custom_send(msg_out_queue, topic, f"time~{time}~{addTimeText}")
def send_date(msg_out_queue, topic, date):
custom_send(msg_out_queue, topic, f"date~{date}")
def entityUpd(msg_out_queue, topic, data):
custom_send(msg_out_queue, topic, f"entityUpd~{data}")
def weatherUpdate(msg_out_queue, topic, data):
custom_send(msg_out_queue, topic, f"weatherUpdate~{data}")
def timeout(msg_out_queue, topic, timeout):
custom_send(msg_out_queue, topic, f"timeout~{timeout}")
def dimmode(msg_out_queue, topic, dimValue, dimValueNormal, backgroundColor, fontColor, featExperimentalSliders):
if dimValue==dimValueNormal:
dimValue=dimValue-1
custom_send(msg_out_queue, topic, f"dimmode~{dimValue}~{dimValueNormal}~{backgroundColor}~{fontColor}~{featExperimentalSliders}")
def entityUpdateDetail(msg_out_queue, topic, data):
custom_send(msg_out_queue, topic, f"entityUpdateDetail~{data}")
def entityUpdateDetail2(msg_out_queue, topic, data):
custom_send(msg_out_queue, topic, f"entityUpdateDetail2~{data}")
def statusUpdate(msg_out_queue, topic, data):
custom_send(msg_out_queue, topic, f"statusUpdate~{data}")
def send_message_page(msg_out_queue, topic, ident, heading, msg, b1, b2):
page_type(msg_out_queue, topic, "popupNotify")
custom_send(msg_out_queue, topic, f"entityUpdateDetail~{ident}~{heading}~65535~{b1}~65535~{b2}~65535~{msg}~65535~0")

View File

@@ -1,224 +1,197 @@
#!/usr/bin/env python
import logging
import paho.mqtt.client as mqtt
import time
import json
import subprocess
import libs.home_assistant
import libs.panel_cmd
import yaml
from uuid import getnode as get_mac
from panel import LovelaceUIPanel
import os
import threading
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
import signal
import sys
from queue import Queue
logging.getLogger("watchdog").propagate = False
settings = {}
panels = {}
panel_queues = {}
last_settings_file_mtime = 0
mqtt_connect_time = 0
has_sent_reload_command = False
mqtt_client_name = "NSPanelLovelaceManager_" + str(get_mac())
client = mqtt.Client(mqtt_client_name)
logging.basicConfig(level=logging.DEBUG)
def on_connect(client, userdata, flags, rc):
global settings
logging.info("Connected to MQTT Server")
# subscribe to panelRecvTopic of each panel
for settings_panel in settings["nspanels"].values():
client.subscribe(settings_panel["panelRecvTopic"])
def on_ha_update(entity_id):
global panel_queues
for queue in panel_queues.values():
queue.put(("HA:", entity_id))
def on_message(client, userdata, msg):
global panel_queues
try:
if msg.payload.decode() == "":
return
parts = msg.topic.split('/')
if msg.topic in panel_queues.keys():
data = json.loads(msg.payload.decode('utf-8'))
if "CustomRecv" in data:
queue = panel_queues[msg.topic]
queue.put(("MQTT:", data["CustomRecv"]))
else:
logging.debug("Received unhandled message on topic: %s", msg.topic)
except Exception: # pylint: disable=broad-exception-caught
logging.exception("Something went wrong during processing of message:")
try:
logging.error(msg.payload.decode('utf-8'))
except: # pylint: disable=bare-except
logging.error(
"Something went wrong when processing the exception message, couldn't decode payload to utf-8.")
def get_config_file():
CONFIG_FILE = os.getenv('CONFIG_FILE')
if not CONFIG_FILE:
CONFIG_FILE = './panels.yaml'
return CONFIG_FILE
def get_config(file):
global settings
try:
with open(file, 'r', encoding="utf8") as file:
settings = yaml.safe_load(file)
except yaml.YAMLError as exc:
print ("Error while parsing YAML file:")
if hasattr(exc, 'problem_mark'):
if exc.context != None:
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + ' ' + str(exc.context) +
'\nPlease correct data and retry.')
else:
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + '\nPlease correct data and retry.')
else:
print ("Something went wrong while parsing yaml file")
return False
if not settings.get("mqtt_username"):
settings["mqtt_username"] = os.getenv('MQTT_USER')
if not settings.get("mqtt_password"):
settings["mqtt_password"] = os.getenv('MQTT_PASS')
if not settings.get("mqtt_port"):
settings["mqtt_port"] = os.getenv('MQTT_PORT')
if not settings.get("mqtt_server"):
settings["mqtt_server"] = os.getenv('MQTT_SERVER')
settings["is_addon"] = False
if not settings.get("home_assistant_token"):
st = os.getenv('SUPERVISOR_TOKEN')
if st and "home_assistant_token" not in settings and "home_assistant_address" not in settings:
settings["home_assistant_token"] = st
settings["home_assistant_address"] = "http://supervisor"
settings["is_addon"] = True
return True
def connect():
global settings, home_assistant, client
client.on_connect = on_connect
client.on_message = on_message
client.username_pw_set(
settings["mqtt_username"], settings["mqtt_password"])
# Wait for connection
connection_return_code = 0
mqtt_server = settings["mqtt_server"]
mqtt_port = int(settings["mqtt_port"])
logging.info("Connecting to %s:%i as %s",
mqtt_server, mqtt_port, mqtt_client_name)
while True:
try:
client.connect(mqtt_server, mqtt_port, 5)
break # Connection call did not raise exception, connection is sucessfull
except: # pylint: disable=bare-except
logging.exception(
"Failed to connect to MQTT %s:%i. Will try again in 10 seconds. Code: %s", mqtt_server, mqtt_port, connection_return_code)
time.sleep(10.)
# MQTT Connected, start APIs if configured
if settings["home_assistant_address"] != "" and settings["home_assistant_token"] != "":
libs.home_assistant.init(settings, on_ha_update)
libs.home_assistant.connect()
else:
logging.info("Home Assistant values not configured, will not connect.")
libs.panel_cmd.init(client)
setup_panels()
def loop():
global client
# Loop MQTT
client.loop_forever()
def setup_panels():
global settings, panel_queues
# Create NsPanel object
for name, settings_panel in settings["nspanels"].items():
if "timeZone" not in settings_panel:
settings_panel["timeZone"] = settings.get("timeZone", "Europe/Berlin")
if "locale" not in settings_panel:
settings_panel["timezone"] = settings.get("locale", "en_US")
if "hiddenCards" not in settings_panel:
settings_panel["hiddenCards"] = settings.get("hiddenCards", [])
#panels[name] = LovelaceUIPanel(name, settings_panel)
mqtt_queue = Queue(maxsize=20)
panel_queues[settings_panel["panelRecvTopic"]] = mqtt_queue
panel_thread = threading.Thread(target=panel_thread_target, args=(mqtt_queue, name, settings_panel))
panel_thread.daemon = True
panel_thread.start()
def panel_thread_target(queue, name, settings_panel):
panel = LovelaceUIPanel(name, settings_panel)
while True:
msg = queue.get()
#print(msg)
if msg[0] == "MQTT:":
panel.customrecv_event_callback(msg[1])
elif msg[0] == "HA:":
panel.ha_event_callback(msg[1])
def config_watch():
class ConfigChangeEventHandler(FileSystemEventHandler):
def __init__(self, base_paths):
self.base_paths = base_paths
def dispatch(self, event):
for base_path in self.base_paths:
if event.src_path.endswith(base_path):
super(ConfigChangeEventHandler, self).dispatch(event)
return
def on_modified(self, event):
logging.info('Modification detected. Reloading panels.')
pid = os.getpid()
os.kill(pid, signal.SIGTERM)
logging.info('Watching for changes in config file')
project_files = []
project_files.append(get_config_file())
handler = ConfigChangeEventHandler(project_files)
observer = Observer()
observer.schedule(handler, path=os.path.dirname(get_config_file()), recursive=True)
observer.start()
while True:
time.sleep(1)
def signal_handler(signum, frame):
logging.info(f"Received signal {signum}. Initiating restart...")
python = sys.executable
os.execl(python, python, *sys.argv)
if __name__ == '__main__':
signal.signal(signal.SIGTERM, signal_handler)
threading.Thread(target=config_watch).start()
if (get_config(get_config_file())):
connect()
loop()
else:
while True:
#!/usr/bin/env python
import logging
import time
import subprocess
import libs.home_assistant
import libs.panel_cmd
import yaml
from panel import LovelaceUIPanel
import os
import threading
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
import signal
import sys
from queue import Queue
from mqtt import MqttManager
logging.getLogger("watchdog").propagate = False
settings = {}
panels = {}
panel_in_queues = {}
panel_out_queue = Queue(maxsize=20)
last_settings_file_mtime = 0
mqtt_connect_time = 0
has_sent_reload_command = False
logging.basicConfig(level=logging.DEBUG)
def on_ha_update(entity_id):
global panel_in_queues
# send HA updates to all panels
for queue in panel_in_queues.values():
queue.put(("HA:", entity_id))
def on_ha_panel_event(device_id, msg):
global panel_in_queues
if device_id in panel_in_queues.keys():
queue = panel_in_queues[device_id]
queue.put(("MQTT:", msg))
def process_output_to_panel():
while True:
msg = panel_out_queue.get()
#client.publish(msg[0], msg[1])
#apis.ha_api.call_service(service="esphome/" + self._api_panel_name + "_nspanelui_api_call", command=2, data=msg)
service = msg[0] + "_nspanelui_api_call"
service_data = {
"data": msg[1],
"command":2
}
libs.home_assistant.send_msg_to_panel(
service = service,
service_data = service_data
)
def connect():
global settings, panel_out_queue
if "mqtt_server" in settings and not "use_ha_api" in settings:
MqttManager(settings, panel_out_queue, panel_in_queues)
else:
logging.info("MQTT values not configured, will not connect.")
# MQTT Connected, start APIs if configured
if settings["home_assistant_address"] != "" and settings["home_assistant_token"] != "":
libs.home_assistant.init(settings, on_ha_update)
libs.home_assistant.connect()
else:
logging.info("Home Assistant values not configured, will not connect.")
while not libs.home_assistant.ws_connected:
time.sleep(1)
if settings.get("use_ha_api"):
libs.home_assistant.subscribe_to_nspanel_events(on_ha_panel_event)
send_to_panel_thread = threading.Thread(target=process_output_to_panel, args=())
send_to_panel_thread.daemon = True
send_to_panel_thread.start()
def setup_panels():
global settings, panel_in_queues
# Create NsPanel object
for name, settings_panel in settings["nspanels"].items():
if "timeZone" not in settings_panel:
settings_panel["timeZone"] = settings.get("timeZone", "Europe/Berlin")
if "locale" not in settings_panel:
settings_panel["timezone"] = settings.get("locale", "en_US")
if "hiddenCards" not in settings_panel:
settings_panel["hiddenCards"] = settings.get("hiddenCards", [])
msg_in_queue = Queue(maxsize=20)
panel_in_queues[settings_panel["panelRecvTopic"]] = msg_in_queue
panel_thread = threading.Thread(target=panel_thread_target, args=(msg_in_queue, name, settings_panel, panel_out_queue))
panel_thread.daemon = True
panel_thread.start()
def panel_thread_target(queue_in, name, settings_panel, queue_out):
panel = LovelaceUIPanel(name, settings_panel, queue_out)
while True:
msg = queue_in.get()
if msg[0] == "MQTT:":
panel.customrecv_event_callback(msg[1])
elif msg[0] == "HA:":
panel.ha_event_callback(msg[1])
def get_config_file():
CONFIG_FILE = os.getenv('CONFIG_FILE')
if not CONFIG_FILE:
CONFIG_FILE = './panels.yaml'
return CONFIG_FILE
def get_config(file):
global settings
try:
with open(file, 'r', encoding="utf8") as file:
settings = yaml.safe_load(file)
except yaml.YAMLError as exc:
print ("Error while parsing YAML file:")
if hasattr(exc, 'problem_mark'):
if exc.context != None:
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + ' ' + str(exc.context) +
'\nPlease correct data and retry.')
else:
print (' parser says\n' + str(exc.problem_mark) + '\n ' +
str(exc.problem) + '\nPlease correct data and retry.')
else:
print ("Something went wrong while parsing yaml file")
return False
if not settings.get("mqtt_username"):
settings["mqtt_username"] = os.getenv('MQTT_USER')
if not settings.get("mqtt_password"):
settings["mqtt_password"] = os.getenv('MQTT_PASS')
if not settings.get("mqtt_port"):
settings["mqtt_port"] = os.getenv('MQTT_PORT')
if not settings.get("mqtt_server"):
settings["mqtt_server"] = os.getenv('MQTT_SERVER')
settings["is_addon"] = False
if not settings.get("home_assistant_token"):
st = os.getenv('SUPERVISOR_TOKEN')
if st and "home_assistant_token" not in settings and "home_assistant_address" not in settings:
settings["home_assistant_token"] = st
settings["home_assistant_address"] = "http://supervisor"
settings["is_addon"] = True
return True
def config_watch():
class ConfigChangeEventHandler(FileSystemEventHandler):
def __init__(self, base_paths):
self.base_paths = base_paths
def dispatch(self, event):
for base_path in self.base_paths:
if event.src_path.endswith(base_path):
super(ConfigChangeEventHandler, self).dispatch(event)
return
def on_modified(self, event):
logging.info('Modification detected. Reloading panels.')
pid = os.getpid()
os.kill(pid, signal.SIGTERM)
logging.info('Watching for changes in config file')
project_files = []
project_files.append(get_config_file())
handler = ConfigChangeEventHandler(project_files)
observer = Observer()
observer.schedule(handler, path=os.path.dirname(get_config_file()), recursive=True)
observer.start()
while True:
time.sleep(1)
def signal_handler(signum, frame):
logging.info(f"Received signal {signum}. Initiating restart...")
python = sys.executable
os.execl(python, python, *sys.argv)
if __name__ == '__main__':
signal.signal(signal.SIGTERM, signal_handler)
threading.Thread(target=config_watch).start()
if (get_config(get_config_file())):
connect()
setup_panels()
# main thread sleep forever
while True:
time.sleep(100)
else:
while True:
time.sleep(100)

View File

@@ -0,0 +1,68 @@
from uuid import getnode as get_mac
import paho.mqtt.client as mqtt
import logging
import time
import json
import threading
class MqttManager:
def __init__(self, settings, msg_in_queue, msg_out_queue_list):
mqtt_client_name = "NSPanelLovelaceManager_" + str(get_mac())
self.client = mqtt.Client(mqtt_client_name)
self.msg_in_queue = msg_in_queue
self.msg_out_queue_list = msg_out_queue_list
self.settings = settings
self.client.on_connect = self.on_mqtt_connect
self.client.on_message = self.on_mqtt_message
self.client.username_pw_set(
settings["mqtt_username"], settings["mqtt_password"])
# Wait for connection
connection_return_code = 0
mqtt_server = settings["mqtt_server"]
mqtt_port = int(settings["mqtt_port"])
logging.info("Connecting to %s:%i as %s",
mqtt_server, mqtt_port, mqtt_client_name)
while True:
try:
self.client.connect(mqtt_server, mqtt_port, 5)
break # Connection call did not raise exception, connection is sucessfull
except: # pylint: disable=bare-except
logging.exception(
"Failed to connect to MQTT %s:%i. Will try again in 10 seconds. Code: %s", mqtt_server, mqtt_port, connection_return_code)
time.sleep(10.)
self.client.loop_start()
process_thread = threading.Thread(target=self.process_in_queue, args=(self.client, self.msg_in_queue))
process_thread.daemon = True
process_thread.start()
def on_mqtt_connect(self, client, userdata, flags, rc):
logging.info("Connected to MQTT Server")
# subscribe to panelRecvTopic of each panel
for settings_panel in self.settings["nspanels"].values():
client.subscribe(settings_panel["panelRecvTopic"])
def on_mqtt_message(self, client, userdata, msg):
try:
if msg.payload.decode() == "":
return
if msg.topic in self.msg_out_queue_list.keys():
data = json.loads(msg.payload.decode('utf-8'))
if "CustomRecv" in data:
queue = self.msg_out_queue_list[msg.topic]
queue.put(("MQTT:", data["CustomRecv"]))
else:
logging.debug("Received unhandled message on topic: %s", msg.topic)
except Exception: # pylint: disable=broad-exception-caught
logging.exception("Something went wrong during processing of message:")
try:
logging.error(msg.payload.decode('utf-8'))
except: # pylint: disable=bare-except
logging.error(
"Something went wrong when processing the exception message, couldn't decode payload to utf-8.")
def process_in_queue(self, client, msg_in_queue):
while True:
msg = msg_in_queue.get()
client.publish(msg[0], msg[1])

View File

@@ -7,18 +7,20 @@ from scheduler import Scheduler
import scheduler.trigger as trigger
import time
import babel.dates
from ha_cards import Screensaver, EntitiesCard, card_factory, detail_open
from ha_cards import Screensaver, card_factory, detail_open
import ha_control
class LovelaceUIPanel:
def __init__(self, name_panel, settings_panel):
def __init__(self, name_panel, settings_panel, msg_out_queue):
self.name = name_panel
self.settings = settings_panel
self.msg_out_queue = msg_out_queue
self.sendTopic = self.settings["panelSendTopic"]
self.recvTopic = self.settings["panelRecvTopic"]
self.model = self.settings.get("model", "eu")
self.temp_unit = self.settings.get("temp_unit", "celsius")
self.current_card = None
self.privious_cards = []
@@ -74,6 +76,10 @@ class LovelaceUIPanel:
ha_control.wait_for_ha_cache()
#request templates on cards
if isinstance(self.settings.get("sleepBrightness",""), str) and self.settings.get("sleepBrightness", "").startswith("ha:"):
libs.home_assistant.cache_template(self.settings.get("sleepBrightness"))
if isinstance(self.settings.get("sleepBrightness",""), str) and self.settings.get("screenBrightness", "").startswith("ha:"):
libs.home_assistant.cache_template(self.settings.get("screenBrightness"))
for c in self.cards.values():
if hasattr(c, "qrcode"):
if c.qrcode.startswith("ha:"):
@@ -94,7 +100,7 @@ class LovelaceUIPanel:
for e in self.screensaver.entities:
e.prerender()
libs.panel_cmd.page_type(self.sendTopic, "pageStartup")
libs.panel_cmd.page_type(self.msg_out_queue, self.sendTopic, "pageStartup")
def schedule_thread_target(self):
@@ -106,13 +112,13 @@ class LovelaceUIPanel:
use_timezone = tz.gettz(self.settings["timeZone"])
time_string = datetime.datetime.now(
use_timezone).strftime(self.settings["timeFormat"])
libs.panel_cmd.send_time(self.sendTopic, time_string)
libs.panel_cmd.send_time(self.msg_out_queue, self.sendTopic, time_string)
def update_date(self):
dateformat = self.settings["dateFormat"]
date_string = babel.dates.format_date(
datetime.datetime.now(), dateformat, locale=self.settings["locale"])
libs.panel_cmd.send_date(self.sendTopic, date_string)
libs.panel_cmd.send_date(self.msg_out_queue, self.sendTopic, date_string)
def searchCard(self, iid):
if iid in self.navigate_keys:
@@ -129,13 +135,23 @@ class LovelaceUIPanel:
# send update for detail popup in case it's open
etype = entity_id.split('.')[0]
if etype in ['light', 'timer', 'cover', 'input_select', 'select', 'fan']:
if etype in ['light', 'timer', 'cover', 'input_select', 'select', 'fan', 'climate']:
# figure out iid of entity
entity_id_iid = ""
for e in self.current_card.get_iid_entities():
if entity_id == e[1]:
entity_id_iid = f'iid.{e[0]}'
libs.panel_cmd.entityUpdateDetail(self.sendTopic, detail_open(self.settings["locale"], etype, entity_id, entity_id_iid, sendTopic=self.sendTopic))
for e in self.current_card.entities:
if entity_id == e.entity_id:
entity_id_iid = f'iid.{e.iid}'
effectList = None
if etype=="light":
effectList = e.config.get("effectList")
if etype == 'light':
libs.panel_cmd.entityUpdateDetail2(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], "popupInSel", entity_id, entity_id_iid, self.msg_out_queue, sendTopic=self.sendTopic, options_list=effectList))
libs.panel_cmd.entityUpdateDetail(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], "popupLight", entity_id, entity_id_iid, self.msg_out_queue, sendTopic=self.sendTopic))
elif etype in ['input_select', 'media_player']:
libs.panel_cmd.entityUpdateDetail2(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], etype, entity_id, entity_id_iid, self.msg_out_queue, sendTopic=self.sendTopic))
else:
libs.panel_cmd.entityUpdateDetail(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], etype, entity_id, entity_id_iid, self.msg_out_queue, sendTopic=self.sendTopic))
involved_entities = ha_control.calculate_dim_values(
self.settings.get("sleepTracking"),
@@ -150,16 +166,12 @@ class LovelaceUIPanel:
def render_current_page(self, switchPages=False, requested=False):
if not self.current_card:
return
if switchPages:
libs.panel_cmd.page_type(self.sendTopic, self.current_card.type)
libs.panel_cmd.page_type(self.msg_out_queue, self.sendTopic, self.current_card.type)
if requested:
self.current_card.render()
# send sleepTimeout
#sleepTimeout = self.settings.get("sleepTimeout", 20)
#if self.current_card.config.get("sleepTimeout"):
# sleepTimeout = self.current_card.config.get("sleepTimeout")
#libs.panel_cmd.timeout(self.sendTopic, sleepTimeout)
#self.dimmode()
def dimmode(self):
# send dimmode
@@ -178,8 +190,15 @@ class LovelaceUIPanel:
backgroundColor = 0
fontColor = ""
featExperimentalSliders = self.settings.get("featExperimentalSliders", 0)
libs.panel_cmd.dimmode(self.sendTopic, dimValue, dimValueNormal, backgroundColor, fontColor, featExperimentalSliders)
libs.panel_cmd.dimmode(self.msg_out_queue, self.sendTopic, dimValue, dimValueNormal, backgroundColor, fontColor, featExperimentalSliders)
def get_default_card(self):
defaultCard = self.settings.get("defaultCard")
if defaultCard and "." in defaultCard:
card = self.searchCard(defaultCard.split(".")[1])
if card:
return card
return list(self.cards.values())[0]
def customrecv_event_callback(self, msg):
logging.debug("Recv Message from NsPanel (%s): %s", self.name, msg)
@@ -198,7 +217,7 @@ class LovelaceUIPanel:
sleepTimeout = self.settings.get("sleepTimeout", 20)
if self.current_card.config.get("sleepTimeout"):
sleepTimeout = self.current_card.config.get("sleepTimeout")
libs.panel_cmd.timeout(self.sendTopic, sleepTimeout)
libs.panel_cmd.timeout(self.msg_out_queue, self.sendTopic, sleepTimeout)
self.dimmode()
if msg[1] == "sleepReached":
@@ -212,13 +231,17 @@ class LovelaceUIPanel:
btype = msg[3]
value = msg[4] if len(msg) > 4 else None
if btype == "bExit":
if entity_id=="screensaver" and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
if entity_id in ["screensaver", "screensaver2"] and self.settings.get("screensaver").get("doubleTapToUnlock") and value == "1":
return
# in case privious_cards is empty add a default card
if len(self.privious_cards) == 0:
self.privious_cards.append(
list(self.cards.values())[0]) # TODO: Impelement default card config
self.privious_cards.append(self.get_default_card())
if self.settings.get("defaultCard") and entity_id in ["screensaver", "screensaver2"]:
logging.debug("Defaulting to card %s", self.settings.get("defaultCard"))
self.privious_cards = [self.get_default_card()]
self.current_card = self.privious_cards.pop()
self.render_current_page(switchPages=True)
return
@@ -226,8 +249,10 @@ class LovelaceUIPanel:
# replace iid with real entity id
if entity_id.startswith("iid."):
iid = entity_id.split(".")[1]
if iid in self.entity_iids:
entity_id = self.entity_iids[iid]
for e in self.current_card.entities:
if e.iid == iid:
entity_id = e.entity_id
entity_config = e.config
match btype:
case 'button':
@@ -236,8 +261,9 @@ class LovelaceUIPanel:
case 'navigate':
card_iid = entity_id.split(".")[1]
if card_iid == "UP":
if len(self.privious_cards) == 0:
self.privious_cards.append(self.get_default_card())
self.current_card = self.privious_cards.pop()
# TODO Handle privious_cards empty with default card
self.render_current_page(switchPages=True)
else:
self.privious_cards.append(self.current_card)
@@ -245,7 +271,7 @@ class LovelaceUIPanel:
self.render_current_page(switchPages=True)
# send ha stuff to ha
case _:
ha_control.handle_buttons(entity_id, btype, value)
ha_control.handle_buttons(entity_id, btype, value, entity_config=entity_config)
case 'cardUnlock-unlock':
card_iid = entity_id.split(".")[1]
if int(self.current_card.config.get("pin")) == int(value):
@@ -260,6 +286,15 @@ class LovelaceUIPanel:
# replace iid with real entity id
if entity_id.startswith("iid."):
iid = entity_id.split(".")[1]
if iid in self.entity_iids:
entity_id = self.entity_iids[iid]
libs.panel_cmd.entityUpdateDetail(self.sendTopic, detail_open(self.settings["locale"], msg[2], entity_id, msg[3], sendTopic=self.sendTopic))
for e in self.current_card.entities:
if e.iid == iid:
entity_id = e.entity_id
effectList = None
if entity_id.startswith("light"):
effectList = e.config.get("effectList")
if msg[2] == "popupInSel": #entity_id.split(".")[0] in ['input_select', 'media_player']:
libs.panel_cmd.entityUpdateDetail2(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], msg[2], entity_id, msg[3], self.msg_out_queue, sendTopic=self.sendTopic, options_list=effectList))
else:
libs.panel_cmd.entityUpdateDetail(self.msg_out_queue, self.sendTopic, detail_open(self.settings["locale"], msg[2], entity_id, msg[3], self.msg_out_queue, sendTopic=self.sendTopic))