This commit is contained in:
Patrick Haßel 2024-11-20 10:43:05 +01:00
parent 3e487c10b5
commit db9db83eca
35 changed files with 1371 additions and 160 deletions

View File

@ -1,4 +1,6 @@
logging.level.de.ph87.home.tvheadend.TvheadendService=DEBUG
#logging.level.de.ph87.home.knx=DEBUG
#logging.level.de.ph87.home.property=DEBUG
#logging.level.de.ph87.home.tvheadend=DEBUG
#-
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
spring.datasource.driverClassName=org.h2.Driver

237
data/G Normal file
View File

@ -0,0 +1,237 @@
<?xml version="1.0" encoding="utf-8"?>
<GAs>
<GRs>
<GR Id="P-02E5-0_GR-1" RangeStart="1" RangeEnd="2047" Name="Haus" Puid="18">
<GR Id="P-02E5-0_GR-2" RangeStart="1" RangeEnd="255" Name="Neue Mittelgruppe" Puid="19">
<GA Id="P-02E5-0_GA-12" Address="12" Name="Haus / Datum, Uhrzeit" Description="" DatapointType="DPST-19-1" Puid="47" />
<GA Id="P-02E5-0_GA-41" Address="21" Name="Haus / Szene" Comment="{\rtf1\ansi\ansicpg1252\uc1\htmautsp\deff2{\fonttbl{\f0\fcharset0 Times New Roman;}{\f2\fcharset0 Segoe UI;}}{\colortbl\red0\green0\blue0;\red255\green255\blue255;}\loch\hich\dbch\pard\plain\ltrpar\itap0{\lang1031\fs16\f2\cf0 \cf0\ql{\f2 {\ltrch 01: Alles AUS}\li0\ri0\sa0\sb0\fi0\ql\par}&#xD;&#xA;{\f2 \li0\ri0\sa0\sb0\fi0\ql\par}&#xD;&#xA;{\f2 {\ltrch 20: Sonnenschutz Front}\li0\ri0\sa0\sb0\fi0\ql\par}&#xD;&#xA;{\f2 \li0\ri0\sa0\sb0\fi0\ql\par}&#xD;&#xA;{\f2 {\ltrch 30: Dekoration AUS}\li0\ri0\sa0\sb0\fi0\ql\par}&#xD;&#xA;{\f2 {\ltrch 30: Dekoration AN}\li0\ri0\sa0\sb0\fi0\ql\par}&#xD;&#xA;}&#xD;&#xA;}" Description="" DatapointType="DPST-17-1" Puid="148" />
<GA Id="P-02E5-0_GA-43" Address="1" Name="Erdgeschoss / Szene" Description="" DatapointType="DPST-17-1" Puid="156" />
<GA Id="P-02E5-0_GA-48" Address="22" Name="Haus / Tag, Nacht" Description="" DatapointType="DPST-1-2" Puid="178" />
<GA Id="P-02E5-0_GA-86" Address="28" Name="Wohnzimmer / Licht / Vorne / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="284" />
<GA Id="P-02E5-0_GA-108" Address="4" Name="Wohnzimmer / Fernseher / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="347" />
<GA Id="P-02E5-0_GA-109" Address="20" Name="Wohnzimmer / Fernseher / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="349" />
</GR>
<GR Id="P-02E5-0_GR-5" RangeStart="768" RangeEnd="1023" Name="Neue Mittelgruppe" Puid="331">
<GA Id="P-02E5-0_GA-107" Address="770" Name="Obergeschoss / Szene" Description="" DatapointType="DPST-17-1" Puid="336" />
<GA Id="P-02E5-0_GA-111" Address="772" Name="Wohnzimmer / Szene" Description="" DatapointType="DPST-17-1" Puid="354" />
<GA Id="P-02E5-0_GA-116" Address="773" Name="Bad / Licht / Dusche / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="378" />
<GA Id="P-02E5-0_GA-117" Address="774" Name="Bad / Licht / Dusche / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="380" />
<GA Id="P-02E5-0_GA-124" Address="781" Name="Bad / Licht / Badewanne / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="395" />
<GA Id="P-02E5-0_GA-125" Address="782" Name="Bad / Licht / Badewanne / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="397" />
<GA Id="P-02E5-0_GA-132" Address="789" Name="Bad / Licht / Waschbecken / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="412" />
<GA Id="P-02E5-0_GA-133" Address="790" Name="Bad / Licht / Waschbecken / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="414" />
<GA Id="P-02E5-0_GA-140" Address="797" Name="Bad / Licht / Mitte / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="429" />
<GA Id="P-02E5-0_GA-141" Address="798" Name="Bad / Licht / Mitte / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="431" />
<GA Id="P-02E5-0_GA-156" Address="813" Name="Flur OG / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="463" />
<GA Id="P-02E5-0_GA-157" Address="814" Name="Flur OG / Licht / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="465" />
<GA Id="P-02E5-0_GA-158" Address="815" Name="Flur OG / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="467" />
<GA Id="P-02E5-0_GA-172" Address="829" Name="Schlafzimmer / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="497" />
<GA Id="P-02E5-0_GA-174" Address="831" Name="Schlafzimmer / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="501" />
<GA Id="P-02E5-0_GA-175" Address="832" Name="Schlafzimmer / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="503" />
<GA Id="P-02E5-0_GA-181" Address="822" Name="Schlafzimmer / Rollläden / Fahren" Description="" DatapointType="DPST-1-8" Puid="539" />
<GA Id="P-02E5-0_GA-182" Address="823" Name="Schlafzimmer / Rollläden / Schritt/Stop" Description="" DatapointType="DPST-1-7" Puid="541" />
<GA Id="P-02E5-0_GA-185" Address="826" Name="Wohnzimmer / Rollläden / Fahren" Description="" DatapointType="DPST-1-8" Puid="548" />
<GA Id="P-02E5-0_GA-186" Address="827" Name="Wohnzimmer / Rollläden / Schritt/Stop" Description="" DatapointType="DPST-1-7" Puid="550" />
<GA Id="P-02E5-0_GA-192" Address="839" Name="Bad / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="569" />
<GA Id="P-02E5-0_GA-193" Address="840" Name="Bad / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="571" />
<GA Id="P-02E5-0_GA-194" Address="841" Name="Bad / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="581" />
<GA Id="P-02E5-0_GA-196" Address="843" Name="Schlafzimmer / Löwe / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="586" />
<GA Id="P-02E5-0_GA-197" Address="844" Name="Schlafzimmer / Löwe / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="588" />
<GA Id="P-02E5-0_GA-198" Address="845" Name="Schlafzimmer / Sternenhimmel / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="590" />
<GA Id="P-02E5-0_GA-199" Address="846" Name="Schlafzimmer / Sternenhimmel / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="592" />
<GA Id="P-02E5-0_GA-201" Address="824" Name="Wohnzimmer / Verstärker / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="638" />
<GA Id="P-02E5-0_GA-202" Address="825" Name="Wohnzimmer / Verstärker / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="640" />
<GA Id="P-02E5-0_GA-203" Address="828" Name="Wohnzimmer / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="642" />
<GA Id="P-02E5-0_GA-204" Address="837" Name="Wohnzimmer / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="644" />
<GA Id="P-02E5-0_GA-205" Address="847" Name="Wohnzimmer / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="646" />
<GA Id="P-02E5-0_GA-206" Address="848" Name="Erdgeschoss / Ambiente / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="648" />
<GA Id="P-02E5-0_GA-207" Address="849" Name="Erdgeschoss / Ambiente / Status / Lesen" Description="" DatapointType="DPST-1-11" Puid="650" />
<GA Id="P-02E5-0_GA-233" Address="768" Name="Schlafzimmer / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="719" />
<GA Id="P-02E5-0_GA-234" Address="769" Name="Bad / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="722" />
<GA Id="P-02E5-0_GA-259" Address="776" Name="Schlafzimmer / Szene" Description="" DatapointType="DPST-17-1" Puid="791" />
</GR>
<GR Id="P-02E5-0_GR-6" RangeStart="1024" RangeEnd="1279" Name="Neue Mittelgruppe" Puid="561">
<GA Id="P-02E5-0_GA-190" Address="1024" Name="Terrasse / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="562" />
<GA Id="P-02E5-0_GA-191" Address="1025" Name="Terrasse / Licht / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="564" />
<GA Id="P-02E5-0_GA-215" Address="1032" Name="Flur EG / Licht / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="672" />
<GA Id="P-02E5-0_GA-221" Address="1038" Name="Erdgeschoss / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="684" />
<GA Id="P-02E5-0_GA-232" Address="1047" Name="Flur OG / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="716" />
<GA Id="P-02E5-0_GA-248" Address="1048" Name="Wohnzimmer / Rollladen / Links / Position" Description="" DatapointType="DPST-5-1" Puid="759" />
<GA Id="P-02E5-0_GA-272" Address="1049" Name="Wohnzimmer / Licht / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="851" />
<GA Id="P-02E5-0_GA-282" Address="1035" Name="Vorgarten / Steckdosen / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="960" />
<GA Id="P-02E5-0_GA-283" Address="1036" Name="Vorgarten / Steckdosen / Status / Lesen" Description="" DatapointType="DPST-1-11" Puid="962" />
</GR>
<GR Id="P-02E5-0_GR-7" RangeStart="1280" RangeEnd="1535" Name="Neue Mittelgruppe" Puid="704">
<GA Id="P-02E5-0_GA-228" Address="1280" Name="Waschküche / Licht / Neon / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="705" />
<GA Id="P-02E5-0_GA-229" Address="1281" Name="Waschküche / Licht / Neon / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="707" />
<GA Id="P-02E5-0_GA-235" Address="1282" Name="Haus / Überwachung / Überstrom" Description="" DatapointType="DPST-1-5" Puid="724" />
<GA Id="P-02E5-0_GA-236" Address="1283" Name="Haus / Überwachung / Übertemperatur" Description="" DatapointType="DPST-1-5" Puid="726" />
<GA Id="P-02E5-0_GA-237" Address="1284" Name="Haus / Überwachung / In Betrieb" Description="" DatapointType="DPST-1-11" Puid="728" />
<GA Id="P-02E5-0_GA-238" Address="1285" Name="Haus / Überwachung / Spannungsversorgung" Description="" DatapointType="DPST-1-11" Puid="734" />
<GA Id="P-02E5-0_GA-239" Address="1286" Name="Haus / Überwachung / Helligkeit" Description="" DatapointType="DPST-9-4" Puid="738" />
<GA Id="P-02E5-0_GA-242" Address="1289" Name="Bad / Szene" Description="" DatapointType="DPST-17-1" Puid="746" />
<GA Id="P-02E5-0_GA-243" Address="1290" Name="Flur OG / Rollladen / Vorne / Fahren" Description="" DatapointType="DPST-1-8" Puid="749" />
<GA Id="P-02E5-0_GA-244" Address="1291" Name="Flur OG / Rollladen / Vorne / Stopp" Description="" DatapointType="DPST-1-7" Puid="751" />
<GA Id="P-02E5-0_GA-246" Address="1293" Name="Flur OG / Rollladen / Vorne / Position anfahren" Description="" DatapointType="DPST-5-1" Puid="755" />
<GA Id="P-02E5-0_GA-254" Address="1294" Name="Flur EG / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="778" />
<GA Id="P-02E5-0_GA-255" Address="1295" Name="Flur EG / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="780" />
<GA Id="P-02E5-0_GA-256" Address="1296" Name="Flur EG / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="782" />
<GA Id="P-02E5-0_GA-258" Address="1297" Name="Küche / Szene" Description="" DatapointType="DPST-17-1" Puid="788" />
<GA Id="P-02E5-0_GA-260" Address="1298" Name="Flur EG / Szene" Description="" DatapointType="DPST-17-1" Puid="794" />
<GA Id="P-02E5-0_GA-261" Address="1299" Name="Bad / Licht / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="805" />
<GA Id="P-02E5-0_GA-286" Address="1287" Name="Schlafzimmer / Rollladen / Sperren" Description="" DatapointType="DPST-1-1" Puid="980" />
<GA Id="P-02E5-0_GA-322" Address="1292" Name="Wohnzimmer / Licht / Szene" Description="" DatapointType="DPST-17-1" Puid="1130" />
<GA Id="P-02E5-0_GA-328" Address="1300" Name="Bad / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1167" />
<GA Id="P-02E5-0_GA-329" Address="1301" Name="Schlafzimmer / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1169" />
<GA Id="P-02E5-0_GA-330" Address="1302" Name="Flur OG / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1171" />
<GA Id="P-02E5-0_GA-331" Address="1305" Name="Emil / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1173" />
<GA Id="P-02E5-0_GA-332" Address="1306" Name="Arbeitszimmer / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1175" />
<GA Id="P-02E5-0_GA-349" Address="1309" Name="Schlafzimmer / Licht / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1254" />
<GA Id="P-02E5-0_GA-377" Address="1288" Name="Bad / Rollladen / Fahren" Description="" DatapointType="DPST-1-8" Puid="1365" />
<GA Id="P-02E5-0_GA-378" Address="1307" Name="Bad / Rollladen / Position Anfahren" Description="" DatapointType="DPST-5-1" Puid="1367" />
<GA Id="P-02E5-0_GA-379" Address="1308" Name="Bad / Rollladen / Position Status" Description="" DatapointType="DPST-5-1" Puid="1369" />
<GA Id="P-02E5-0_GA-380" Address="1310" Name="Bad / Rollladen / Richtung Status" Description="" DatapointType="DPST-1-8" Puid="1371" />
<GA Id="P-02E5-0_GA-381" Address="1311" Name="Bad / Rollladen / Stopp" Description="" DatapointType="DPST-1-1" Puid="1373" />
<GA Id="P-02E5-0_GA-382" Address="1312" Name="Bad / Licht / Spiegel / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1386" />
<GA Id="P-02E5-0_GA-383" Address="1313" Name="Bad / Licht / Spiegel / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="1388" />
<GA Id="P-02E5-0_GA-384" Address="1314" Name="Bad / Licht / Spiegel / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="1390" />
<GA Id="P-02E5-0_GA-423" Address="1303" Name="Schlafzimmer / Rollladen / Links / Position Anfahren" Description="" DatapointType="DPST-5-1" Puid="1495" />
<GA Id="P-02E5-0_GA-424" Address="1304" Name="Schlafzimmer / Rollladen / Rechts / Position Anfahren" Description="" DatapointType="DPST-5-1" Puid="1497" />
<GA Id="P-02E5-0_GA-435" Address="1315" Name="Schlafzimmer / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1561" />
<GA Id="P-02E5-0_GA-436" Address="1316" Name="Schlafzimmer / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1563" />
<GA Id="P-02E5-0_GA-441" Address="1317" Name="Bad / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1573" />
<GA Id="P-02E5-0_GA-442" Address="1318" Name="Bad / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1575" />
<GA Id="P-02E5-0_GA-443" Address="1319" Name="Bad / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1577" />
<GA Id="P-02E5-0_GA-444" Address="1320" Name="Bad / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1579" />
<GA Id="P-02E5-0_GA-449" Address="1321" Name="Schlafzimmer / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1593" />
<GA Id="P-02E5-0_GA-450" Address="1322" Name="Schlafzimmer / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1595" />
</GR>
<GR Id="P-02E5-0_GR-8" RangeStart="1536" RangeEnd="1791" Name="Neue Mittelgruppe" Puid="769">
<GA Id="P-02E5-0_GA-252" Address="1538" Name="Obergeschoss / Ambiente / Status / Lesen" Description="" DatapointType="DPST-1-11" Puid="774" />
<GA Id="P-02E5-0_GA-253" Address="1539" Name="Obergeschoss / Ambiente / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="776" />
<GA Id="P-02E5-0_GA-257" Address="1540" Name="Flur OG / Szene" Description="" DatapointType="DPST-17-1" Puid="785" />
<GA Id="P-02E5-0_GA-266" Address="1545" Name="Flur OG / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="820" />
<GA Id="P-02E5-0_GA-278" Address="1542" Name="Obergeschoss / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="883" />
</GR>
<GR Id="P-02E5-0_GR-10" RangeStart="1792" RangeEnd="2047" Name="Neue Mittelgruppe" Puid="885">
<GA Id="P-02E5-0_GA-279" Address="1792" Name="Emil / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="886" />
<GA Id="P-02E5-0_GA-288" Address="1795" Name="Emil / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="984" />
<GA Id="P-02E5-0_GA-289" Address="1796" Name="Emil / Licht / Status / Lesen" Description="" DatapointType="DPST-1-11" Puid="986" />
<GA Id="P-02E5-0_GA-290" Address="1797" Name="Emil / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="988" />
<GA Id="P-02E5-0_GA-291" Address="1798" Name="Emil / Szene" Description="" DatapointType="DPST-17-1" Puid="995" />
<GA Id="P-02E5-0_GA-293" Address="1800" Name="Emil / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="1000" />
<GA Id="P-02E5-0_GA-294" Address="1801" Name="Emil / Steckdose Fenster Rechts / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1006" />
<GA Id="P-02E5-0_GA-295" Address="1802" Name="Emil / Steckdose Fenster Rechts / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1008" />
<GA Id="P-02E5-0_GA-296" Address="1803" Name="Emil / Steckdose Fenster Links / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1010" />
<GA Id="P-02E5-0_GA-297" Address="1804" Name="Emil / Steckdose Fenster Links / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1012" />
<GA Id="P-02E5-0_GA-298" Address="1805" Name="Emil / Rollladen / Fahren" Description="" DatapointType="DPST-1-8" Puid="1014" />
<GA Id="P-02E5-0_GA-299" Address="1806" Name="Emil / Rollladen / Stopp" Description="" DatapointType="DPST-1-1" Puid="1016" />
<GA Id="P-02E5-0_GA-300" Address="1807" Name="Emil / Rollladen / Position Anfahren" Description="" DatapointType="DPST-5-1" Puid="1018" />
<GA Id="P-02E5-0_GA-320" Address="1808" Name="Flur EG / Licht / Szene" Description="" DatapointType="DPST-17-1" Puid="1093" />
<GA Id="P-02E5-0_GA-323" Address="1809" Name="Flur EG / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1153" />
<GA Id="P-02E5-0_GA-324" Address="1810" Name="Wohnzimmer / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1155" />
<GA Id="P-02E5-0_GA-326" Address="1812" Name="Küche / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1159" />
<GA Id="P-02E5-0_GA-327" Address="1813" Name="Waschküche / Langzeitpräsenz" Description="" DatapointType="DPST-1-2" Puid="1161" />
<GA Id="P-02E5-0_GA-333" Address="1814" Name="Flur EG / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="1181" />
<GA Id="P-02E5-0_GA-334" Address="1815" Name="Wohnzimmer / Gesamt / Status" Description="" DatapointType="DPST-1-2" Puid="1183" />
<GA Id="P-02E5-0_GA-335" Address="1816" Name="Waschküche / Gesamt / Status" Description="" DatapointType="DPST-1-2" Puid="1185" />
<GA Id="P-02E5-0_GA-336" Address="1817" Name="Küche / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="1187" />
<GA Id="P-02E5-0_GA-360" Address="1793" Name="Wohnzimmer / Licht / Präsenz Sperren" Description="" DatapointType="DPST-1-3" Puid="1297" />
<GA Id="P-02E5-0_GA-375" Address="1794" Name="Wohnzimmer / Licht Mitte / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1360" />
<GA Id="P-02E5-0_GA-376" Address="1799" Name="Wohnzimmer / Licht Mitte / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1362" />
<GA Id="P-02E5-0_GA-402" Address="1818" Name="Waschküche / Rollladen / Fahren" Description="" DatapointType="DPST-1-8" Puid="1437" />
<GA Id="P-02E5-0_GA-403" Address="1819" Name="Waschküche / Rollladen / Stopp" Description="" DatapointType="DPST-1-7" Puid="1439" />
<GA Id="P-02E5-0_GA-404" Address="1820" Name="Waschküche / Rollladen / Position" Description="" DatapointType="DPST-5-1" Puid="1441" />
<GA Id="P-02E5-0_GA-405" Address="1821" Name="Waschküche / Szene" Description="" DatapointType="DPST-17-1" Puid="1443" />
<GA Id="P-02E5-0_GA-425" Address="1811" Name="Wohnzimmer / Rollladen / Rechts / Position" Description="" DatapointType="DPST-5-1" Puid="1523" />
<GA Id="P-02E5-0_GA-426" Address="1822" Name="Wohnzimmer / Weihnachtsbeleuchtung / Status / Setzen" DatapointType="DPST-1-1" Puid="1531" />
<GA Id="P-02E5-0_GA-427" Address="1823" Name="Wohnzimmer / Weihnachtsbeleuchtung / Status / Lesen" DatapointType="DPST-1-11" Puid="1533" />
<GA Id="P-02E5-0_GA-431" Address="1824" Name="Wohnzimmer / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1553" />
<GA Id="P-02E5-0_GA-432" Address="1825" Name="Wohnzimmer / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1555" />
<GA Id="P-02E5-0_GA-451" Address="1826" Name="Waschküche / Licht / Spots / Setzen" DatapointType="DPST-1-1" Puid="1597" />
<GA Id="P-02E5-0_GA-452" Address="1827" Name="Waschküche / Licht / Spots / Lesen" DatapointType="DPST-1-11" Puid="1599" />
<GA Id="P-02E5-0_GA-453" Address="1828" Name="Waschküche / Licht / Spots / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1601" />
<GA Id="P-02E5-0_GA-454" Address="1829" Name="Waschküche / Licht / Spots / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1603" />
<GA Id="P-02E5-0_GA-455" Address="1830" Name="Waschküche / Licht / Spots / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1605" />
<GA Id="P-02E5-0_GA-456" Address="1831" Name="Waschküche / Licht / Spots / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1607" />
<GA Id="P-02E5-0_GA-457" Address="1832" Name="Waschküche / Licht / Spots / Helligkeit / Schritt" DatapointType="DPST-3-7" Puid="1609" />
<GA Id="P-02E5-0_GA-458" Address="1833" Name="Waschküche / Licht / Spots / Farbtemperatur / Schritt" DatapointType="DPST-3-7" Puid="1611" />
</GR>
</GR>
<GR Id="P-02E5-0_GR-11" RangeStart="2048" RangeEnd="4095" Name="Neue Hauptgruppe" Puid="1032">
<GR Id="P-02E5-0_GR-12" RangeStart="2048" RangeEnd="2303" Name="Neue Mittelgruppe" Puid="1033">
<GA Id="P-02E5-0_GA-301" Address="2048" Name="Arbeitszimmer / Schreibtisch / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1034" />
<GA Id="P-02E5-0_GA-303" Address="2050" Name="Arbeitszimmer / Schreibtisch / Status / Lesen" Description="" DatapointType="DPST-1-11" Puid="1038" />
<GA Id="P-02E5-0_GA-304" Address="2051" Name="Arbeitszimmer / PC / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1040" />
<GA Id="P-02E5-0_GA-305" Address="2052" Name="Arbeitszimmer / PC / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1043" />
<GA Id="P-02E5-0_GA-306" Address="2053" Name="Arbeitszimmer / Szene" Description="" DatapointType="DPST-17-1" Puid="1045" />
<GA Id="P-02E5-0_GA-308" Address="2055" Name="Emil / Licht / Szene" Description="" DatapointType="DPST-17-1" Puid="1054" />
<GA Id="P-02E5-0_GA-309" Address="2056" Name="Arbeitszimmer / Licht / Szene" Description="" DatapointType="DPST-17-1" Puid="1063" />
<GA Id="P-02E5-0_GA-310" Address="2057" Name="Arbeitszimmer / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1065" />
<GA Id="P-02E5-0_GA-311" Address="2058" Name="Arbeitszimmer / Licht / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="1067" />
<GA Id="P-02E5-0_GA-312" Address="2059" Name="Arbeitszimmer / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="1069" />
<GA Id="P-02E5-0_GA-313" Address="2060" Name="Arbeitszimmer / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="1071" />
<GA Id="P-02E5-0_GA-314" Address="2061" Name="Arbeitszimmer / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="1073" />
<GA Id="P-02E5-0_GA-315" Address="2062" Name="Arbeitszimmer / Rollladen / Fahren" Description="" DatapointType="DPST-1-8" Puid="1076" />
<GA Id="P-02E5-0_GA-316" Address="2063" Name="Arbeitszimmer / Rollladen / Stopp" Description="" DatapointType="DPST-1-1" Puid="1078" />
<GA Id="P-02E5-0_GA-317" Address="2064" Name="Arbeitszimmer / Rollladen / Position Setzen" Description="" DatapointType="DPST-5-1" Puid="1080" />
<GA Id="P-02E5-0_GA-318" Address="2065" Name="Arbeitszimmer / Drucker / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1082" />
<GA Id="P-02E5-0_GA-319" Address="2066" Name="Arbeitszimmer / Drucker / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1084" />
<GA Id="P-02E5-0_GA-354" Address="2071" Name="Emil / Steckdose Wand Scheune / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1280" />
<GA Id="P-02E5-0_GA-355" Address="2072" Name="Emil / Steckdose Wand Scheune / Status / Lesen" Description="" DatapointType="DPST-1-2" Puid="1282" />
<GA Id="P-02E5-0_GA-361" Address="2049" Name="Arbeitszimmer / Licht / Farbtemperatur / Lesen" Description="" DatapointType="DPST-5-1" Puid="1303" />
<GA Id="P-02E5-0_GA-362" Address="2054" Name="Arbeitszimmer / Licht / Farbtemperatur / Setzen" Description="" DatapointType="DPST-5-1" Puid="1305" />
<GA Id="P-02E5-0_GA-363" Address="2067" Name="Arbeitszimmer / Licht / Helligkeit / Lesen" Description="" DatapointType="DPST-5-1" Puid="1307" />
<GA Id="P-02E5-0_GA-364" Address="2069" Name="Arbeitszimmer / Licht / Helligkeit / Setzen" Description="" DatapointType="DPST-5-1" Puid="1309" />
<GA Id="P-02E5-0_GA-371" Address="2075" Name="Emil / Rollladen / Sperren" Description="" DatapointType="DPST-1-1" Puid="1340" />
<GA Id="P-02E5-0_GA-385" Address="2076" Name="Flur OG / Rollladen / Hinten / Fahren" Description="" DatapointType="DPST-1-8" Puid="1392" />
<GA Id="P-02E5-0_GA-386" Address="2077" Name="Flur OG / Rollladen / Hinten / Stopp" Description="" DatapointType="DPST-1-17" Puid="1394" />
<GA Id="P-02E5-0_GA-387" Address="2078" Name="Flur OG / Rollladen / Hinten / Richtung Status" Description="" DatapointType="DPST-1-8" Puid="1396" />
<GA Id="P-02E5-0_GA-388" Address="2079" Name="Flur OG / Rollladen / Hinten / Position Anfahren" Description="" DatapointType="DPST-5-1" Puid="1398" />
<GA Id="P-02E5-0_GA-389" Address="2080" Name="Flur OG / Rollladen / Hinten / Position Status" Description="" DatapointType="DPST-5-1" Puid="1400" />
<GA Id="P-02E5-0_GA-418" Address="2081" Name="Arbeitszimmer / Licht / Bewegung Sperren" Description="" DatapointType="DPST-1-1" Puid="1476" />
<GA Id="P-02E5-0_GA-437" Address="2073" Name="Flur OG / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1565" />
<GA Id="P-02E5-0_GA-438" Address="2074" Name="Flur OG / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1567" />
<GA Id="P-02E5-0_GA-439" Address="2082" Name="Emil / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1569" />
<GA Id="P-02E5-0_GA-440" Address="2083" Name="Emil / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1571" />
<GA Id="P-02E5-0_GA-445" Address="2084" Name="Emil / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1581" />
<GA Id="P-02E5-0_GA-446" Address="2085" Name="Emil / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1583" />
<GA Id="P-02E5-0_GA-447" Address="2070" Name="Flur OG / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1585" />
<GA Id="P-02E5-0_GA-448" Address="2086" Name="Flur OG / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1587" />
</GR>
<GR Id="P-02E5-0_GR-13" RangeStart="2304" RangeEnd="2559" Name="Neue Mittelgruppe" Puid="1228">
<GA Id="P-02E5-0_GA-356" Address="2306" Name="Außen / Gesamt / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1285" />
<GA Id="P-02E5-0_GA-357" Address="2307" Name="Außen / Szene" Description="" DatapointType="DPST-17-1" Puid="1287" />
<GA Id="P-02E5-0_GA-390" Address="2314" Name="Küche / Rollladen / Seite / Fahren" Description="" DatapointType="DPST-1-8" Puid="1408" />
<GA Id="P-02E5-0_GA-391" Address="2315" Name="Küche / Rollladen / Seite / Stopp" Description="" DatapointType="DPST-1-7" Puid="1410" />
<GA Id="P-02E5-0_GA-392" Address="2316" Name="Küche / Rollladen / Seite / Position Setzen" Description="" DatapointType="DPST-5-1" Puid="1412" />
<GA Id="P-02E5-0_GA-394" Address="2318" Name="Küche / Rollladen / Theke / Fahren" Description="" DatapointType="DPST-1-8" Puid="1416" />
<GA Id="P-02E5-0_GA-395" Address="2319" Name="Küche / Rollladen / Theke / Stopp" Description="" DatapointType="DPST-1-7" Puid="1418" />
<GA Id="P-02E5-0_GA-396" Address="2320" Name="Küche / Rollladen / Theke / Position Setzen" Description="" DatapointType="DPST-5-1" Puid="1420" />
<GA Id="P-02E5-0_GA-398" Address="2322" Name="Küche / Rollladen / Tür / Fahren" Description="" DatapointType="DPST-1-8" Puid="1424" />
<GA Id="P-02E5-0_GA-399" Address="2323" Name="Küche / Rollladen / Tür / Stopp" Description="" DatapointType="DPST-1-7" Puid="1426" />
<GA Id="P-02E5-0_GA-400" Address="2324" Name="Küche / Rollladen / Tür / Position Setzen" Description="" DatapointType="DPST-5-1" Puid="1428" />
<GA Id="P-02E5-0_GA-406" Address="2326" Name="Küche / Rollladen / Tür / Zwang AUF" Description="" DatapointType="DPT-1" Puid="1445" />
<GA Id="P-02E5-0_GA-408" Address="2304" Name="Küche / Licht / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1453" />
<GA Id="P-02E5-0_GA-409" Address="2305" Name="Küche / Licht / Helligkeit / Schritt" Description="" DatapointType="DPST-3-7" Puid="1455" />
<GA Id="P-02E5-0_GA-410" Address="2308" Name="Küche / Licht / Farbtemperatur / Schritt" Description="" DatapointType="DPST-3-7" Puid="1457" />
<GA Id="P-02E5-0_GA-411" Address="2309" Name="Küche / Präsenzmelder / Slave" Description="" DatapointType="DPST-1-1" Puid="1459" />
<GA Id="P-02E5-0_GA-412" Address="2310" Name="Küche / Spots / Status / Lesen" Description="" DatapointType="DPST-1-11" Puid="1461" />
<GA Id="P-02E5-0_GA-413" Address="2311" Name="Küche / Licht / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1464" />
<GA Id="P-02E5-0_GA-414" Address="2312" Name="Küche / Hängelampe / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="1466" />
<GA Id="P-02E5-0_GA-415" Address="2313" Name="Küche / Hängelampe / Status / Lesen" Description="" DatapointType="DPST-1-1" Puid="1468" />
<GA Id="P-02E5-0_GA-416" Address="2328" Name="Küche / Licht / Szene" Description="" DatapointType="DPST-17-1" Puid="1470" />
<GA Id="P-02E5-0_GA-417" Address="2329" Name="Treppe / Präsenzmelder / Langzeitpräsenz" Description="" DatapointType="DPST-1-1" Puid="1473" />
<GA Id="P-02E5-0_GA-429" Address="2317" Name="Küche / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1549" />
<GA Id="P-02E5-0_GA-430" Address="2321" Name="Küche / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1551" />
<GA Id="P-02E5-0_GA-433" Address="2325" Name="Flur EG / Licht / Farbtemperatur / Setzen" DatapointType="DPST-5-1" Puid="1557" />
<GA Id="P-02E5-0_GA-434" Address="2327" Name="Flur EG / Licht / Farbtemperatur / Lesen" DatapointType="DPST-5-1" Puid="1559" />
<GA Id="P-02E5-0_GA-459" Address="2330" Name="Haus / Überwachung / Bewegung" DatapointType="DPST-1-1" Puid="1613" />
</GR>
<GR Id="P-02E5-0_GR-14" RangeStart="2560" RangeEnd="2815" Name="Neue Mittelgruppe" Puid="1616">
<GA Id="P-02E5-0_GA-460" Address="2560" Name="Keller / Sat. Receiver / Status / Setzen" DatapointType="DPST-1-1" Puid="1617" />
<GA Id="P-02E5-0_GA-461" Address="2561" Name="Keller / Sat. Receiver / Status / Lesen" DatapointType="DPST-1-11" Puid="1619" />
</GR>
</GR>
</GRs>
</GAs>

10
pom.xml
View File

@ -44,6 +44,16 @@
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>com.github.calimero</groupId>
<artifactId>calimero-core</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>

View File

@ -0,0 +1,42 @@
package de.ph87.home.demo;
import de.ph87.home.device.DeviceService;
import de.ph87.home.knx.property.KnxPropertyService;
import de.ph87.home.knx.property.KnxPropertyType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tuwien.auto.calimero.GroupAddress;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class DemoService {
private final KnxPropertyService knxPropertyService;
private final DeviceService deviceService;
@EventListener(ApplicationStartedEvent.class)
public void startup() {
knxPropertyService.create("fernseher", KnxPropertyType.BOOLEAN, adr(20), adr(4));
knxPropertyService.create("verstaerker", KnxPropertyType.BOOLEAN, adr(825), adr(824));
knxPropertyService.create("receiver", KnxPropertyType.BOOLEAN, adr(2561), adr(2560));
deviceService.create("EG Ambiente", "eg_ambiente", "eg_ambiente");
deviceService.create("Wohnzimmer Fernseher", "fernseher", "fernseher");
deviceService.create("Wohnzimmer Verstärker", "verstaerker", "verstaerker");
deviceService.create("Wohnzimmer Fensterdeko", "fensterdeko", "fensterdeko");
deviceService.create("Wohnzimmer Hängelampe", "haengelampe", "haengelampe");
deviceService.create("Receiver", "receiver", "receiver");
}
private static GroupAddress adr(final int rawGroupAddress) {
return GroupAddress.freeStyle(rawGroupAddress);
}
}

View File

@ -22,7 +22,7 @@ public class DeviceController {
@NonNull
@RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST})
private List<DeviceDto> list(@RequestBody(required = false) @Nullable final DeviceFilter filter, @NonNull final HttpServletRequest request) {
private List<DeviceDto> list(@RequestBody(required = false) @Nullable final DeviceFilter filter, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch {
log.debug("list: path={} filter={}", request.getServletPath(), filter);
return deviceService.list(filter);
}
@ -36,7 +36,7 @@ public class DeviceController {
@Nullable
@GetMapping("getState/{uuidOrSlug}")
private Boolean getState(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws DeviceNotFound {
private Boolean getState(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws DeviceNotFound, PropertyTypeMismatch {
log.debug("getState: path={}", request.getServletPath());
return deviceService.toDto(uuidOrSlug).getStateValue();
}

View File

@ -1,6 +1,7 @@
package de.ph87.home.device;
import de.ph87.home.property.State;
import de.ph87.home.property.PropertyDto;
import de.ph87.home.property.PropertyTypeMismatch;
import jakarta.annotation.Nullable;
import lombok.Getter;
import lombok.NonNull;
@ -20,25 +21,36 @@ public class DeviceDto {
private final String slug;
@NonNull
private final String stateProperty;
private final String stateConfig;
@Nullable
private final State<Boolean> state;
@ToString.Exclude
private final PropertyDto<Boolean> state;
public DeviceDto(@NonNull final Device device, @Nullable final State<Boolean> state) {
public DeviceDto(@NonNull final Device device, @Nullable final PropertyDto<Boolean> state) {
this.uuid = device.getUuid();
this.name = device.getName();
this.slug = device.getSlug();
this.stateProperty = device.getStateProperty();
this.stateConfig = device.getStateProperty();
this.state = state;
}
@Nullable
public Boolean getStateValue() {
@ToString.Include
public String state() {
try {
return "" + getStateValue();
} catch (PropertyTypeMismatch e) {
return "[PropertyTypeMismatch]";
}
}
@Nullable
public Boolean getStateValue() throws PropertyTypeMismatch {
if (state == null) {
return null;
}
return state.getValue();
return state.getStateValueAs(Boolean.class);
}
}

View File

@ -1,21 +0,0 @@
package de.ph87.home.device;
import de.ph87.home.property.PropertyDto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@Getter
@ToString
@RequiredArgsConstructor
public class DeviceEvent {
private final DeviceDto deviceDto;
private final PropertyDto<?> propertyDto;
public boolean isValueDifferent() {
return propertyDto.isValueChanged();
}
}

View File

@ -1,6 +1,7 @@
package de.ph87.home.device;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.ph87.home.property.PropertyTypeMismatch;
import jakarta.annotation.Nullable;
import lombok.Getter;
import lombok.NonNull;
@ -23,14 +24,15 @@ public class DeviceFilter {
private Boolean stateFalse;
@SuppressWarnings("RedundantIfStatement")
public boolean filter(@NonNull final DeviceDto dto) {
public boolean filter(@NonNull final DeviceDto dto) throws PropertyTypeMismatch {
if (stateNull != null && stateNull != (dto.getState() == null)) {
return false;
}
if (stateTrue != null && (dto.getState() == null || stateTrue != dto.getState().getValue())) {
final Boolean value = dto.getStateValue();
if (stateTrue != null && (dto.getState() == null || stateTrue != value)) {
return false;
}
if (stateFalse != null && (dto.getState() == null || stateFalse == dto.getState().getValue())) {
if (stateFalse != null && (dto.getState() == null || stateFalse == value)) {
return false;
}
return true;

View File

@ -2,7 +2,6 @@ package de.ph87.home.device;
import de.ph87.home.property.*;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -11,6 +10,7 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@ -25,54 +25,57 @@ public class DeviceService {
private final ApplicationEventPublisher applicationEventPublisher;
@PostConstruct
public void postConstruct() {
deviceRepository.save(new Device("EG Ambiente", "eg_ambiente", "eg_ambiente"));
deviceRepository.save(new Device("Wohnzimmer Fernseher", "wohnzimmer_fernseher", "wohnzimmer_fernseher"));
deviceRepository.save(new Device("Wohnzimmer Verstärker", "wohnzimmer_verstaerker", "wohnzimmer_verstaerker"));
deviceRepository.save(new Device("Wohnzimmer Fensterdeko", "wohnzimmer_fensterdeko", "wohnzimmer_fensterdeko"));
deviceRepository.save(new Device("Wohnzimmer Hängelampe", "wohnzimmer_haengelampe", "wohnzimmer_haengelampe"));
@NonNull
public DeviceDto create(@NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty) {
return toDto(deviceRepository.save(new Device(name, slug, stateProperty)));
}
public void setState(@NonNull final String uuidOrSlug, final boolean state) throws DeviceNotFound, PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch {
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
final Device device = byUuidOrSlug(uuidOrSlug);
log.debug("setState: device={}", device);
final Device device = getByUuidOrSlug(uuidOrSlug);
propertyService.write(device.getStateProperty(), state, Boolean.class);
}
@NonNull
public DeviceDto toDto(final @NonNull String uuidOrSlug) throws DeviceNotFound {
return toDto(byUuidOrSlug(uuidOrSlug));
return toDto(getByUuidOrSlug(uuidOrSlug));
}
@NonNull
public DeviceDto toDto(@NonNull final Device device) {
final State<Boolean> state = propertyService.readSafe(device.getStateProperty(), Boolean.class);
final PropertyDto<Boolean> state = propertyService.dtoByIdAndTypeOrNull(device.getStateProperty(), Boolean.class);
return new DeviceDto(device, state);
}
@NonNull
private Device byUuidOrSlug(@NonNull final String uuidOrSlug) throws DeviceNotFound {
private Device getByUuidOrSlug(@NonNull final String uuidOrSlug) throws DeviceNotFound {
return deviceRepository.findByUuidOrSlug(uuidOrSlug, uuidOrSlug).orElseThrow(() -> new DeviceNotFound("uuidOrSlug", uuidOrSlug));
}
@NonNull
public List<DeviceDto> list(@Nullable final DeviceFilter filter) {
return deviceRepository.findAll().stream().map(this::toDto).filter(device -> filter == null || filter.filter(device)).toList();
public List<DeviceDto> list(@Nullable final DeviceFilter filter) throws PropertyTypeMismatch {
final List<DeviceDto> all = deviceRepository.findAll().stream().map(this::toDto).toList();
if (filter == null) {
return all;
}
final List<DeviceDto> results = new ArrayList<>();
for (final DeviceDto dto : all) {
if (filter.filter(dto)) {
results.add(dto);
}
}
return results;
}
@EventListener(PropertyDto.class)
public void onPropertyChange(@NonNull final PropertyDto<?> dto) {
deviceRepository.findAllByStateProperty(dto.getId())
.forEach(device -> {
final DeviceEvent deviceEvent = new DeviceEvent(toDto(device), dto);
log.debug("Device updated: {}", deviceEvent.getDeviceDto());
if (deviceEvent.isValueDifferent()) {
log.info("Device changed: {}", deviceEvent.getDeviceDto());
}
applicationEventPublisher.publishEvent(deviceEvent);
});
deviceRepository.findAllByStateProperty(dto.getId()).forEach(this::publish);
}
private void publish(@NonNull final Device device) {
final DeviceDto deviceDto = toDto(device);
log.info("Device updated: {}", deviceDto);
applicationEventPublisher.publishEvent(deviceDto);
}
}

View File

@ -1,31 +0,0 @@
package de.ph87.home.dummy;
import de.ph87.home.property.PropertyService;
import de.ph87.home.property.State;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class DummyService {
private final PropertyService propertyService;
@PostConstruct
public void postConstruct() {
register("eg_ambiente");
register("wohnzimmer_fernseher");
register("wohnzimmer_verstaerker");
register("wohnzimmer_fensterdeko");
register("wohnzimmer_haengelampe");
register("receiver");
}
private void register(final String id) {
propertyService.register(id, Boolean.class, (property, value) -> property.setState(new State<>(value)));
}
}

View File

@ -0,0 +1,45 @@
package de.ph87.home.knx;
import lombok.Getter;
import lombok.NonNull;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.TranslatorTypes;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter
public class DPT {
public static final Pattern DPT_REGEX = Pattern.compile("^DPS?T-(?<main>\\d+)(?:-(?<sub>\\d+))?$");
public final int main;
public final int sub;
public DPT(@NonNull final String dpt) throws DPTException {
final Matcher matcher = DPT_REGEX.matcher(dpt);
if (!matcher.find()) {
throw new DPTException(dpt);
}
try {
main = Integer.parseInt(matcher.group("main"));
final String subStr = matcher.group("sub");
sub = Integer.parseInt(subStr == null ? "1" : subStr);
} catch (NumberFormatException e) {
throw new DPTException(dpt, e);
}
}
@Override
public String toString() {
return "%d.%03d".formatted(main, sub);
}
@NonNull
public DPTXlator createTranslator() throws KNXException {
return TranslatorTypes.createTranslator(main, toString());
}
}

View File

@ -0,0 +1,15 @@
package de.ph87.home.knx;
import lombok.NonNull;
public class DPTException extends Exception {
public DPTException(@NonNull final String dpt) {
super("Failed to parse DPT: " + dpt);
}
public DPTException(@NonNull final String dpt, @NonNull final Exception inner) {
super("Failed to parse DPT: " + dpt, inner);
}
}

View File

@ -0,0 +1,67 @@
package de.ph87.home.knx;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import tuwien.auto.calimero.GroupAddress;
import java.util.Objects;
@Getter
@ToString
public class Group {
@NonNull
@ToString.Exclude
private String id;
@NonNull
private final GroupAddress address;
@NonNull
private String name;
@NonNull
@ToString.Exclude
private String description;
@NonNull
private DPT dpt;
@ToString.Exclude
private long puid;
public Group(@NonNull final String id, @NonNull final GroupAddress address, @NonNull final String name, @NonNull final String description, @NonNull final DPT dpt, final long puid) {
this.id = id;
this.address = address;
this.name = name;
this.description = description;
this.dpt = dpt;
this.puid = puid;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof final Group group)) {
return false;
}
return Objects.equals(address, group.address);
}
@Override
public int hashCode() {
return Objects.hashCode(address);
}
public void merge(@NonNull final Group group) {
this.id = group.id;
this.name = group.name;
this.description = group.description;
this.dpt = group.dpt;
this.puid = group.puid;
}
}

View File

@ -0,0 +1,31 @@
package de.ph87.home.knx;
import io.micrometer.common.lang.Nullable;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import lombok.SneakyThrows;
import tuwien.auto.calimero.GroupAddress;
@Converter(autoApply = true)
public class GroupAddressJpaConverter implements AttributeConverter<GroupAddress, String> {
@Nullable
@Override
public String convertToDatabaseColumn(@Nullable final GroupAddress groupAddress) {
if (groupAddress == null) {
return null;
}
return groupAddress.toString();
}
@Nullable
@Override
@SneakyThrows
public GroupAddress convertToEntityAttribute(@Nullable final String string) {
if (string == null) {
return null;
}
return GroupAddress.from(string);
}
}

View File

@ -0,0 +1,16 @@
package de.ph87.home.knx;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public class GroupLoaded {
private final Group group;
public GroupLoaded(final Group group) {
this.group = group;
}
}

View File

@ -0,0 +1,80 @@
package de.ph87.home.knx;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXFormatException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupService {
private final List<Group> groupList = new ArrayList<>();
private final ApplicationEventPublisher applicationEventPublisher;
@EventListener(ApplicationStartedEvent.class)
public void load() {
try {
final Document document = Jsoup.parse(new File("./data/G"));
final Elements gaList = document.select("GA");
gaList.stream().map(this::load).filter(Optional::isPresent).map(Optional::get).forEach(this::merge);
} catch (IOException e) {
log.error("Failed to load groups: {}", e.getMessage());
}
}
@NonNull
private Optional<Group> load(@NonNull final Element element) {
try {
final String id = element.attr("Id");
final GroupAddress address = GroupAddress.from(element.attr("Address"));
final String name = element.attr("Name");
final String description = element.attr("Description");
final DPT dpt = new DPT(element.attr("DatapointType"));
final long puid = Long.parseLong(element.attr("Puid"));
final Group group = new Group(id, address, name, description, dpt, puid);
log.info("Group loaded: {}", group);
return Optional.of(group);
} catch (KNXFormatException | DPTException e) {
log.error(e.getMessage());
return Optional.empty();
}
}
private void merge(@NonNull Group group) {
synchronized (groupList) {
groupList.stream()
.filter(g -> g.equals(group))
.findFirst().ifPresentOrElse(
existing -> existing.merge(group),
() -> groupList.add(group)
);
}
applicationEventPublisher.publishEvent(new GroupLoaded(group));
}
@NonNull
public Optional<Group> findByAddress(@NonNull final GroupAddress address) {
synchronized (groupList) {
return groupList.stream().filter(g -> g.getAddress().equals(address)).findFirst();
}
}
}

View File

@ -0,0 +1,18 @@
package de.ph87.home.knx;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "de.ph87.home.knx")
public class KnxConfig {
private boolean enabled = true;
private String remoteAddress = "10.0.0.102";
private int remotePort = 3671;
}

View File

@ -0,0 +1,184 @@
package de.ph87.home.knx.link;
import de.ph87.home.knx.Group;
import de.ph87.home.knx.KnxConfig;
import de.ph87.home.knx.link.request.Request;
import de.ph87.home.knx.link.request.RequestRead;
import de.ph87.home.knx.link.request.RequestWrite;
import de.ph87.home.knx.link.router.Router;
import jakarta.annotation.PostConstruct;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.datapoint.StateDP;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class KnxLinkService {
private static final Duration RESPONSE_TIMEOUT = Duration.ofMillis(500);
private final KnxConfig knxConfig;
private final ApplicationEventPublisher applicationEventPublisher;
private KNXNetworkLinkIP link = null;
private ProcessCommunicatorImpl processCommunicator = null;
private final List<Request> requests = new ArrayList<>();
@PostConstruct
public void postConstruct() {
if (!knxConfig.isEnabled()) {
return;
}
final Thread thread = new Thread(this::run, getClass().getSimpleName());
thread.setDaemon(true);
thread.start();
}
private void run() {
try {
while (true) {
execute(waitForRequest());
}
} catch (InterruptedException e) {
log.warn("Interrupted");
}
}
public Request waitForRequest() throws InterruptedException {
synchronized (requests) {
while (requests.isEmpty() || processCommunicator == null) {
if (processCommunicator == null) {
connect();
continue;
}
requests.wait();
}
return requests.removeFirst();
}
}
private void execute(@NonNull final Request request) {
try {
log.debug("Executing request: {}", request);
if (request instanceof final RequestRead read) {
processCommunicator.read(new StateDP(read.getGroup().getAddress(), ""));
} else if (request instanceof final RequestWrite write) {
processCommunicator.write(write.getGroup().getAddress(), write.getDptXlator());
} else {
throw new NotImplementedException(Request.class.getSimpleName());
}
} catch (Exception e) {
log.error("KnxRequest failed: request={}, error={}", request, e.getMessage());
processCommunicator = null;
}
}
public void queueRead(@NonNull final Group group) {
if (!knxConfig.isEnabled()) {
return;
}
synchronized (requests) {
if (requests.stream().filter(r -> r instanceof RequestRead read && read.group.equals(group)).findFirst().isEmpty()) {
add(new RequestRead(group));
}
}
}
public void queueWrite(@NonNull final Group group, @NonNull final DPTXlator dptXlator) {
if (!knxConfig.isEnabled()) {
return;
}
synchronized (requests) {
requests.stream().filter(r -> r instanceof RequestWrite write && write.getGroup().equals(group)).map(RequestWrite.class::cast).findFirst().ifPresentOrElse(
request -> request.update(dptXlator),
() -> add(new RequestWrite(group, dptXlator))
);
}
}
private void add(@NonNull final Request request) {
synchronized (requests) {
requests.add(request);
requests.notify();
}
}
public void connect() throws InterruptedException {
try {
final Inet4Address remoteAddress = (Inet4Address) Inet4Address.getByName(knxConfig.getRemoteAddress());
final Inet4Address localAddress = Router.getLocalInet4AddressForRemoteAddress(remoteAddress);
log.info("Connecting KNX link: {} -> {}", localAddress, remoteAddress);
link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress(localAddress, 0), new InetSocketAddress(remoteAddress, knxConfig.getRemotePort()), false, new TPSettings());
processCommunicator = new ProcessCommunicatorImpl(link);
processCommunicator.addProcessListener(new MyProcessListener());
processCommunicator.responseTimeout(RESPONSE_TIMEOUT);
log.info("KNX link established.");
synchronized (requests) {
requests.notify();
}
} catch (IOException | KNXException | InterruptedException e) {
log.error("Failed to connect KNX: {}", e.toString());
synchronized (requests) {
requests.wait(1000);
}
}
}
private class MyProcessListener implements ProcessListener {
@Override
public void groupReadRequest(@NonNull final ProcessEvent processEvent) {
log.debug("groupReadRequest: {}", processEvent);
}
@Override
public void groupReadResponse(@NonNull final ProcessEvent processEvent) {
log.debug("groupReadResponse: {}", processEvent);
applicationEventPublisher.publishEvent(processEvent);
}
@Override
public void groupWrite(@NonNull final ProcessEvent processEvent) {
log.debug("groupWrite: {}", processEvent);
applicationEventPublisher.publishEvent(processEvent);
}
@Override
public void detached(@NonNull final DetachEvent detachEvent) {
log.info("KNX link disconnected.");
synchronized (requests) {
processCommunicator = null;
link = null;
requests.notify();
}
}
}
}

View File

@ -0,0 +1,17 @@
package de.ph87.home.knx.link.request;
import de.ph87.home.knx.Group;
import lombok.Getter;
import lombok.NonNull;
@Getter
public abstract class Request {
@NonNull
public final Group group;
public Request(@NonNull final Group group) {
this.group = group;
}
}

View File

@ -0,0 +1,17 @@
package de.ph87.home.knx.link.request;
import de.ph87.home.knx.Group;
import lombok.NonNull;
public class RequestRead extends Request {
public RequestRead(@NonNull final Group group) {
super(group);
}
@Override
public String toString() {
return "Read(%s, %s, %s)".formatted(group.getAddress(), group.getDpt(), group.getName());
}
}

View File

@ -0,0 +1,28 @@
package de.ph87.home.knx.link.request;
import de.ph87.home.knx.Group;
import lombok.Getter;
import lombok.NonNull;
import tuwien.auto.calimero.dptxlator.DPTXlator;
@Getter
public class RequestWrite extends Request {
@NonNull
private DPTXlator dptXlator;
public RequestWrite(@NonNull final Group group, @NonNull final DPTXlator dptXlator) {
super(group);
this.dptXlator = dptXlator;
}
public void update(@NonNull final DPTXlator dptXlator) {
this.dptXlator = dptXlator;
}
@Override
public String toString() {
return "Write(%s, %s, %s, %s)".formatted(group.getAddress(), group.getDpt(), group.getName(), dptXlator.getValue());
}
}

View File

@ -0,0 +1,93 @@
package de.ph87.home.knx.link.router;
import lombok.extern.slf4j.Slf4j;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class Route {
private static final Pattern regex = Pattern.compile("^([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+(\\S+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\S+)$");
public final Inet4Address network;
public final Inet4Address router;
public final Inet4Address netmask;
public final int metric;
public final String iface;
public final int networkInt;
public final int routerInt;
public final int netmaskInt;
public final Inet4Address addressFirst;
public final int addressFirstInt;
public final Inet4Address addressLast;
public final int addressLastInt;
public Route(final Inet4Address network, final Inet4Address router, final Inet4Address netmask, final int metric, final String iface) throws UnknownHostException {
this.network = network;
this.router = router;
this.netmask = netmask;
this.metric = metric;
this.iface = iface;
this.networkInt = toInt(network);
this.routerInt = toInt(router);
this.netmaskInt = toInt(netmask);
this.addressFirstInt = networkInt & netmaskInt;
this.addressLastInt = networkInt | ~netmaskInt;
this.addressFirst = fromInt(addressFirstInt);
this.addressLast = fromInt(addressLastInt);
}
public boolean matches(final Inet4Address address) {
final int addressInt = toInt(address);
return addressFirstInt <= addressInt && addressInt <= addressLastInt;
}
private static int toInt(final Inet4Address address) {
return ByteBuffer.wrap(address.getAddress()).getInt();
}
private static Inet4Address fromInt(final int value) throws UnknownHostException {
return (Inet4Address) Inet4Address.getByAddress(ByteBuffer.allocate(4).putInt(value).array());
}
public static Optional<Route> parse(final String line) {
final Matcher matcher = regex.matcher(line);
try {
if (matcher.matches()) {
final String networkString = matcher.group(1);
final Inet4Address network = (Inet4Address) Inet4Address.getByName(Objects.equals(networkString, "default") ? "0.0.0.0" : networkString);
final Inet4Address router = (Inet4Address) Inet4Address.getByName(matcher.group(2));
final Inet4Address netmask = (Inet4Address) Inet4Address.getByName(matcher.group(3));
final int metric = Integer.parseInt(matcher.group(5));
final String iface = matcher.group(8);
return Optional.of(new Route(network, router, netmask, metric, iface));
}
} catch (UnknownHostException e) {
log.error(e.getMessage());
}
return Optional.empty();
}
public boolean isDefault() {
return network.isAnyLocalAddress() && netmask.isAnyLocalAddress();
}
}

View File

@ -0,0 +1,40 @@
package de.ph87.home.knx.link.router;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.NetworkInterface;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
public class Router {
public static Inet4Address getLocalInet4AddressForRemoteAddress(final Inet4Address remoteAddress) throws IOException {
final Route route = getRoute(remoteAddress);
final NetworkInterface networkInterface = NetworkInterface.getByName(route.iface);
return (Inet4Address) networkInterface.inetAddresses().filter(address -> address instanceof Inet4Address).findFirst().orElse(null);
}
public static Route getRoute(final Inet4Address remoteAddress) throws IOException {
final List<Route> routes = execute(Route::parse, "/sbin/route", "-n");
final Optional<Route> routeOptional = routes.stream()
.filter(r -> !r.isDefault())
.filter(r -> r.matches(remoteAddress))
.reduce((a, b) -> a.metric < b.metric ? a : b)
.or(() -> routes.stream()
.filter(Route::isDefault)
.reduce((a, b) -> a.metric < b.metric ? a : b)
);
if (routeOptional.isEmpty()) {
throw new IOException("No route found for remoteAddress: " + remoteAddress);
}
return routeOptional.get();
}
private static <T> List<T> execute(final Function<String, Optional<T>> parser, final String... commands) throws IOException {
final String output = new String(new ProcessBuilder(commands).start().getInputStream().readAllBytes());
return Arrays.stream(output.split("\\n")).map(parser).filter(Optional::isPresent).map(Optional::get).toList();
}
}

View File

@ -0,0 +1,44 @@
package de.ph87.home.knx.property;
import de.ph87.home.knx.GroupAddressJpaConverter;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
import tuwien.auto.calimero.GroupAddress;
@Entity
@Getter
@ToString
@NoArgsConstructor
public class KnxProperty {
@Id
@NonNull
private String id;
@Nullable
@Convert(converter = GroupAddressJpaConverter.class)
private GroupAddress read;
@Nullable
@Convert(converter = GroupAddressJpaConverter.class)
private GroupAddress write;
@NonNull
@Column(nullable = false)
private KnxPropertyType type;
public KnxProperty(@NonNull final String id, @NonNull final KnxPropertyType type, @Nullable final GroupAddress read, @Nullable final GroupAddress write) {
this.id = id;
this.type = type;
this.read = read;
this.write = write;
}
}

View File

@ -0,0 +1,13 @@
package de.ph87.home.knx.property;
import lombok.NonNull;
import org.springframework.data.repository.ListCrudRepository;
import tuwien.auto.calimero.GroupAddress;
import java.util.List;
public interface KnxPropertyRepository extends ListCrudRepository<KnxProperty, String> {
List<KnxProperty> findDistinctByReadOrWrite(@NonNull GroupAddress read, @NonNull GroupAddress write);
}

View File

@ -0,0 +1,136 @@
package de.ph87.home.knx.property;
import de.ph87.home.knx.Group;
import de.ph87.home.knx.GroupLoaded;
import de.ph87.home.knx.GroupService;
import de.ph87.home.knx.link.KnxLinkService;
import de.ph87.home.property.PropertyNotFound;
import de.ph87.home.property.PropertyNotOwned;
import de.ph87.home.property.PropertyService;
import de.ph87.home.property.PropertyTypeMismatch;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
import tuwien.auto.calimero.process.ProcessEvent;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@Transactional
@EnableScheduling
@RequiredArgsConstructor
public class KnxPropertyService {
private final KnxPropertyRepository knxPropertyRepository;
private final PropertyService propertyService;
private final GroupService groupService;
private final KnxLinkService knxLinkService;
@Scheduled(initialDelay = 1, fixedDelay = 1, timeUnit = TimeUnit.HOURS)
public void readAll() {
knxPropertyRepository.findAll().forEach(this::read);
}
@EventListener(GroupLoaded.class)
public void onGroupLoad(@NonNull final GroupLoaded groupLoaded) {
findAllByAddress(groupLoaded.getGroup().getAddress()).forEach(this::read);
}
public void create(@NonNull final String id, @NonNull final KnxPropertyType type, @Nullable final GroupAddress read, @Nullable final GroupAddress write) {
final KnxProperty knxProperty = knxPropertyRepository.save(new KnxProperty(id, type, read, write));
updateOrCreateProperty(knxProperty);
}
private void updateOrCreateProperty(@NonNull final KnxProperty knxProperty) {
try {
propertyService.createOrUpdate(this, knxProperty.getId(), knxProperty.getType().clazz, (p, value) -> write(knxProperty, value));
read(knxProperty);
} catch (PropertyNotOwned e) {
log.error("Failed to register KnxProperty: knxProperty={}, error={}", knxProperty, e.toString());
}
}
@EventListener(ProcessEvent.class)
public void onProcessEvent(@NonNull final ProcessEvent event) {
findAllByAddress(event.getDestination()).forEach(knxProperty -> onProcessEvent(knxProperty, event));
}
@NonNull
private List<KnxProperty> findAllByAddress(@NonNull final GroupAddress address) {
return knxPropertyRepository.findDistinctByReadOrWrite(address, address);
}
private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event) {
log.debug("onProcessEvent: knxProperty={}, event={}", knxProperty, event);
groupService.findByAddress(event.getDestination()).ifPresent(group -> onProcessEvent(knxProperty, event, group));
}
private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event, @NonNull final Group group) {
log.debug("onProcessEvent: knxProperty={}, group={}, event={}", knxProperty, group, event);
try {
final DPTXlator translator = group.getDpt().createTranslator();
translator.setData(event.getASDU());
log.debug("translator: {}", translator);
switch (knxProperty.getType()) {
case BOOLEAN -> {
if (!(translator instanceof final DPTXlatorBoolean booleanTranslator)) {
throw new RuntimeException("DPTXlator type should be DPTXlatorBoolean for property.type = BOOLEAN but is: " + translator.getClass().getSimpleName());
}
propertyService.update(this, knxProperty.getId(), Boolean.class, booleanTranslator.getValueBoolean(), booleanTranslator.getValue());
}
case DOUBLE -> propertyService.update(this, knxProperty.getId(), Double.class, translator.getNumericValue(), translator.getValue());
}
} catch (KNXException | PropertyNotFound | PropertyTypeMismatch | PropertyNotOwned e) {
log.error("Failed to handle ProcessEvent: knxProperty={}, error={}", knxProperty, e.toString());
}
}
private void read(@NonNull final KnxProperty knxProperty) {
final Optional<KnxProperty> property = knxPropertyRepository.findById(knxProperty.getId());
final Optional<GroupAddress> address = property.map(KnxProperty::getRead);
final Optional<Group> group = address.map(groupService::findByAddress).filter(Optional::isPresent).map(Optional::get);
group.ifPresent(knxLinkService::queueRead);
}
private void write(@NonNull final KnxProperty knxProperty, @NonNull final Object value) {
knxPropertyRepository.findById(knxProperty.getId()).map(KnxProperty::getWrite).map(groupService::findByAddress).filter(Optional::isPresent).map(Optional::get).ifPresent(group -> write(knxProperty, group, value));
}
private void write(@NonNull final KnxProperty knxProperty, @NonNull final Group group, @NonNull final Object value) {
try {
if (!knxProperty.getType().clazz.isInstance(value)) {
throw new RuntimeException("Cannot write invalid value type: type=%s, value=%s, knxProperty=%s".formatted(value.getClass(), value, knxProperty));
}
final DPTXlator translator = group.getDpt().createTranslator();
switch (knxProperty.getType()) {
case BOOLEAN -> {
if (!(translator instanceof final DPTXlatorBoolean booleanTranslator)) {
throw new RuntimeException("DPTXlator type should be DPTXlatorBoolean for property.type = BOOLEAN but is: " + translator.getClass().getSimpleName());
}
booleanTranslator.setValue((boolean) value);
}
case DOUBLE -> translator.setValue((double) value);
}
knxLinkService.queueWrite(group, translator);
} catch (KNXException e) {
log.error("Failed to write KnxProperty: knxProperty={}, error={}", knxProperty, e.toString());
}
}
}

View File

@ -0,0 +1,17 @@
package de.ph87.home.knx.property;
import lombok.NonNull;
public enum KnxPropertyType {
BOOLEAN(Boolean.class),
DOUBLE(Double.class),
;
@NonNull
public final Class<?> clazz;
KnxPropertyType(@NonNull final Class<?> clazz) {
this.clazz = clazz;
}
}

View File

@ -0,0 +1,42 @@
package de.ph87.home.property;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import java.util.Objects;
public interface IProperty<T> {
@NonNull
String getId();
@Nullable
State<T> getState();
@NonNull
Class<T> getType();
@Nullable
default T getStateValue() {
if (getState() == null) {
return null;
}
return getState().getValue();
}
@Nullable
default <R extends T> R getStateValueAs(@NonNull final Class<R> type) throws PropertyTypeMismatch {
if (this.getType() != type) {
throw new PropertyTypeMismatch(this, type);
}
//noinspection unchecked
return (R) getStateValue();
}
@NonNull
default <R extends T> R getStateValueAs(@NonNull final Class<R> type, @NonNull final R fallbackIfNull) throws PropertyTypeMismatch {
final R value = getStateValueAs(type);
return Objects.requireNonNullElse(value, fallbackIfNull);
}
}

View File

@ -13,21 +13,39 @@ import java.util.function.Consumer;
@Getter
@ToString
@RequiredArgsConstructor
public class Property<T> {
public class Property<T> implements IProperty<T> {
@NonNull
@ToString.Exclude
private final Object owner;
@ToString.Include
public String owner() {
return owner.getClass().getSimpleName();
}
@NonNull
private final String id;
@NonNull
@ToString.Exclude
private final Class<T> type;
@ToString.Include
public String type() {
return type.getSimpleName();
}
@Nullable
@ToString.Exclude
private final BiConsumer<Property<T>, T> write;
@NonNull
@ToString.Exclude
private final Consumer<Property<T>> onStateSet;
@Nullable
@ToString.Exclude
private State<T> lastState = null;
@Nullable
@ -35,36 +53,13 @@ public class Property<T> {
private boolean valueChanged = false;
public void setState(@Nullable final State<T> state) {
public void update(@Nullable final State<T> state) {
this.lastState = this.state;
this.state = state;
this.valueChanged = (lastState == null) == (state == null) && (lastState == null || Objects.equals(lastState.getValue(), state.getValue()));
this.onStateSet.accept(this);
}
@Nullable
public T getStateValue() {
if (state == null) {
return null;
}
return state.getValue();
}
@Nullable
public <R extends T> R getStateValueAs(@NonNull final Class<R> type) throws PropertyTypeMismatch {
if (this.type != type) {
throw new PropertyTypeMismatch(this, type);
}
//noinspection unchecked
return (R) getStateValue();
}
@NonNull
public <R extends T> R getStateValueAs(@NonNull final Class<R> type, @NonNull final R fallbackIfNull) throws PropertyTypeMismatch {
final R value = getStateValueAs(type);
return Objects.requireNonNullElse(value, fallbackIfNull);
}
public void write(@NonNull final T value) throws PropertyNotWritable {
if (write == null) {
throw new PropertyNotWritable(this);

View File

@ -7,15 +7,22 @@ import lombok.ToString;
@Getter
@ToString
public class PropertyDto<T> {
public class PropertyDto<T> implements IProperty<T> {
@NonNull
private final String id;
@NonNull
@ToString.Exclude
private final Class<T> type;
@ToString.Include
public String type() {
return type.getSimpleName();
}
@Nullable
@ToString.Exclude
private final State<T> lastState;
@Nullable

View File

@ -0,0 +1,11 @@
package de.ph87.home.property;
import lombok.NonNull;
public class PropertyNotOwned extends Exception {
public PropertyNotOwned(@NonNull final Property<?> property, @NonNull final Object tryingOwner) {
super("Property not owned: property=%s, tryingOwner=%s".formatted(property, tryingOwner));
}
}

View File

@ -21,56 +21,76 @@ public class PropertyService {
private final List<Property<?>> propertyList = new ArrayList<>();
@Nullable
public <TYPE> State<TYPE> readSafe(final @NonNull String id, @NonNull final Class<TYPE> type) {
try {
return this.read(id, type);
} catch (PropertyTypeMismatch | PropertyNotFound e) {
log.error(e.getMessage());
return null;
@SuppressWarnings("UnusedReturnValue")
public <T> Property<T> createOrUpdate(@NonNull final Object owner, @NonNull final String id, final Class<T> type, final BiConsumer<Property<T>, T> write) throws PropertyNotOwned {
if (id.isEmpty()) {
throw new IllegalArgumentException("id cannot be empty");
}
}
@Nullable
public <TYPE> State<TYPE> read(@NonNull final String id, @NonNull final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
log.debug("read: id={}", id);
return byIdAndType(id, type).getState();
}
public <TYPE> void write(@NonNull final String id, @NonNull final TYPE value, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value);
byIdAndType(id, type).write(value);
}
@NonNull
public <TYPE> Property<TYPE> byIdAndType(final String id, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
final Property<?> property = findById(id).orElseThrow(() -> new PropertyNotFound(id));
if (type != property.getType()) {
throw new PropertyTypeMismatch(property, type);
}
//noinspection unchecked
return (Property<TYPE>) property;
}
@NonNull
private Optional<Property<?>> findById(final @NonNull String id) {
final Optional<Property<?>> optional;
synchronized (propertyList) {
optional = propertyList.stream().filter(p -> p.getId().equals(id)).findFirst();
final boolean removed = findByIdAndOwner(id, owner).map(propertyList::remove).orElse(false);
if (propertyList.stream().anyMatch(property -> property.getId().equals(id))) {
throw new RuntimeException();
}
final Property<T> property = new Property<>(owner, id, type, write, this::onStateSet);
propertyList.add(property);
if (removed) {
log.info("Property property: {}", property);
} else {
log.info("Property modified: {}", property);
}
return property;
}
}
public <T> void write(@NonNull final String id, @NonNull final T value, final Class<T> type) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value);
getByIdAndType(id, type).write(value);
}
@NonNull
public <T> Property<T> getByIdAndTypeAndOwner(@NonNull final String id, @NonNull final Class<T> type, @NonNull final Object owner) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotOwned {
final Property<T> property = getByIdAndType(id, type);
if (property.getOwner() != owner) {
throw new PropertyNotOwned(property, owner);
}
return property;
}
@NonNull
public Optional<Property<?>> findByIdAndOwner(@NonNull final String id, @NonNull final Object owner) throws PropertyNotOwned {
final Optional<Property<?>> optional = findById(id);
if (optional.isEmpty()) {
return Optional.empty();
}
if (optional.get().getOwner() != owner) {
throw new PropertyNotOwned(optional.get(), owner);
}
return optional;
}
@SuppressWarnings("UnusedReturnValue")
public <TYPE> Property<TYPE> register(@NonNull final String id, final Class<TYPE> type, final BiConsumer<Property<TYPE>, TYPE> write) {
if (id.isEmpty()) {
throw new RuntimeException();
}
final Property<TYPE> property = new Property<>(id, type, write, this::onStateSet);
@NonNull
public <T> Property<T> getByIdAndType(@NonNull final String id, @NonNull final Class<T> type) throws PropertyNotFound, PropertyTypeMismatch {
synchronized (propertyList) {
propertyList.add(property);
return findByIdAndType(id, type).orElseThrow(() -> new PropertyNotFound(id));
}
return property;
}
private <T> Optional<Property<T>> findByIdAndType(final String id, final Class<T> type) throws PropertyNotFound, PropertyTypeMismatch {
final @NonNull Optional<Property<?>> optional = findById(id);
if (optional.isEmpty()) {
return Optional.empty();
}
final Property<?> property = optional.get();
if (type != property.getType()) {
throw new PropertyTypeMismatch(property, type);
}
//noinspection unchecked
return Optional.of((Property<T>) property);
}
@NonNull
private Optional<Property<?>> findById(final String id) {
return propertyList.stream().filter(p -> p.getId().equals(id)).findFirst();
}
private void onStateSet(@NonNull final Property<?> property) {
@ -87,4 +107,19 @@ public class PropertyService {
return new PropertyDto<>(property);
}
public <T> void update(@NonNull final Object owner, @NonNull final String id, @NonNull final Class<T> type, @Nullable final T value, @NonNull final String string) throws PropertyNotFound, PropertyNotOwned, PropertyTypeMismatch {
final Property<T> property = getByIdAndTypeAndOwner(id, type, owner);
property.update(new State<>(property.getType().cast(value), string));
}
@Nullable
public <T> PropertyDto<T> dtoByIdAndTypeOrNull(final @NonNull String id, final Class<T> type) {
try {
return findByIdAndType(id, type).map(this::toDto).orElse(null);
} catch (PropertyNotFound | PropertyTypeMismatch e) {
log.error(e.getMessage());
return null;
}
}
}

View File

@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class PropertyTypeMismatch extends Exception {
public PropertyTypeMismatch(@NonNull final Property<?> property, @NonNull final Class<?> type) {
public PropertyTypeMismatch(@NonNull final IProperty<?> property, @NonNull final Class<?> type) {
super("Property type mismatch: id=%s, expected=%s, given=%s".formatted(property.getId(), property.getType().getSimpleName(), type.getSimpleName()));
}

View File

@ -17,7 +17,11 @@ public class State<T> {
@Nullable
private final T value;
public State(@Nullable final T value) {
@Nullable
private final String string;
public State(@Nullable final T value, @Nullable final String string) {
this.string = string;
this.timestamp = ZonedDateTime.now();
this.value = value;
}

View File

@ -47,7 +47,7 @@ public class TvheadendService {
final Property<Boolean> receiver;
try {
receiver = propertyService.byIdAndType(tvheadendConfig.getReceiver(), Boolean.class);
receiver = propertyService.getByIdAndType(tvheadendConfig.getReceiver(), Boolean.class);
} catch (PropertyNotFound | PropertyTypeMismatch e) {
log.warn("Failed to retrieve receiver Property: id={}, error={}", tvheadendConfig.getReceiver(), e.getMessage());
return;