Compare commits
23 Commits
expression
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 63830cd58d | |||
| 0936a7bc47 | |||
| 116158ab1f | |||
| 3da3dea353 | |||
| 9023f57c37 | |||
| b3ecb231c8 | |||
| 4fd64acf09 | |||
| a9394efc11 | |||
| dc6d19a633 | |||
| 3970a9a142 | |||
| ea0aa3e00a | |||
| 8133080e9c | |||
| b6f3db79e4 | |||
| 417bf890a0 | |||
| 61ffab50ba | |||
| ad130fc35e | |||
| 120d6fffdc | |||
| dc3e262010 | |||
| 97d558e9c9 | |||
| 27238d73d0 | |||
| b94f602b4b | |||
| 73926b13e6 | |||
| bb2af44542 |
44
data/G
44
data/G
@ -8,8 +8,8 @@
|
||||
<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" />
|
||||
<GA Id="P-02E5-0_GA-108" Address="4" Name="Wohnzimmer / Verstärker / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="347" />
|
||||
<GA Id="P-02E5-0_GA-109" Address="20" Name="Wohnzimmer / Verstärker / 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" />
|
||||
@ -39,8 +39,8 @@
|
||||
<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-201" Address="824" Name="Wohnzimmer / Fernseher / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="638" />
|
||||
<GA Id="P-02E5-0_GA-202" Address="825" Name="Wohnzimmer / Fernseher / 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" />
|
||||
@ -58,8 +58,8 @@
|
||||
<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" />
|
||||
<GA Id="P-02E5-0_GA-282" Address="1035" Name="Vorgarten / Dekoration / Status / Setzen" Description="" DatapointType="DPST-1-1" Puid="960" />
|
||||
<GA Id="P-02E5-0_GA-283" Address="1036" Name="Vorgarten / Dekoration / 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" />
|
||||
@ -95,8 +95,8 @@
|
||||
<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-423" Address="1303" Name="Schlafzimmer / Rollladen / Links / Position" Description="" DatapointType="DPST-5-1" Puid="1495" />
|
||||
<GA Id="P-02E5-0_GA-424" Address="1304" Name="Schlafzimmer / Rollladen / Rechts / Position" 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" />
|
||||
@ -126,7 +126,7 @@
|
||||
<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-300" Address="1807" Name="Emil / Rollladen / Position" 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" />
|
||||
@ -144,8 +144,8 @@
|
||||
<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-426" Address="1822" Name="Wohnzimmer / Fenster Dekoration / Status / Setzen" DatapointType="DPST-1-1" Puid="1531" />
|
||||
<GA Id="P-02E5-0_GA-427" Address="1823" Name="Wohnzimmer / Fenster Dekoration / 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" />
|
||||
@ -174,7 +174,7 @@
|
||||
<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-317" Address="2064" Name="Arbeitszimmer / Rollladen / Position" 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" />
|
||||
@ -204,13 +204,13 @@
|
||||
<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-392" Address="2316" Name="Küche / Rollladen / Seite / Position" 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-396" Address="2320" Name="Küche / Rollladen / Theke / Position" 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-400" Address="2324" Name="Küche / Rollladen / Tür / Position" 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" />
|
||||
@ -227,6 +227,20 @@
|
||||
<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" />
|
||||
<GA Id="P-02E5-0_GA-462" Address="2331" Name="Flur OG / Fenster Dekoration / Lesen" DatapointType="DPST-1-11" Puid="1621" />
|
||||
<GA Id="P-02E5-0_GA-463" Address="2332" Name="Flur OG / Fenster Dekoration / Setzen" DatapointType="DPST-1-1" Puid="1623" />
|
||||
<GA Id="P-02E5-0_GA-464" Address="2333" Name="Schlafzimmer / Fenster Dekoration / Lesen" DatapointType="DPST-1-11" Puid="1625" />
|
||||
<GA Id="P-02E5-0_GA-465" Address="2334" Name="Schlafzimmer / Fenster Dekoration / Setzen" DatapointType="DPST-1-1" Puid="1627" />
|
||||
<GA Id="P-02E5-0_GA-466" Address="2335" Name="Wohnzimmer / Steckdose / Hinter Tür / Lesen" DatapointType="DPST-1-11" Puid="1634" />
|
||||
<GA Id="P-02E5-0_GA-467" Address="2336" Name="Wohnzimmer / Steckdose / Hinter Tür / Setzen" DatapointType="DPST-1-1" Puid="1636" />
|
||||
<GA Id="P-02E5-0_GA-468" Address="2337" Name="Terrasse / Dekoration / Lesen" DatapointType="DPST-1-11" Puid="1638" />
|
||||
<GA Id="P-02E5-0_GA-469" Address="2338" Name="Terrasse / Dekoration / Setzen" DatapointType="DPST-1-1" Puid="1640" />
|
||||
<GA Id="P-02E5-0_GA-470" Address="2339" Name="Flur EG / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1642" />
|
||||
<GA Id="P-02E5-0_GA-471" Address="2340" Name="Flur EG / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1644" />
|
||||
<GA Id="P-02E5-0_GA-472" Address="2341" Name="Küche / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1646" />
|
||||
<GA Id="P-02E5-0_GA-473" Address="2342" Name="Küche / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1648" />
|
||||
<GA Id="P-02E5-0_GA-474" Address="2343" Name="Wohnzimmer / Licht / Helligkeit / Setzen" DatapointType="DPST-5-1" Puid="1652" />
|
||||
<GA Id="P-02E5-0_GA-475" Address="2344" Name="Wohnzimmer / Licht / Helligkeit / Lesen" DatapointType="DPST-5-1" Puid="1654" />
|
||||
</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" />
|
||||
|
||||
48
src/main/angular/package-lock.json
generated
48
src/main/angular/package-lock.json
generated
@ -16,6 +16,8 @@
|
||||
"@angular/platform-browser": "^18.2.0",
|
||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||
"@angular/router": "^18.2.0",
|
||||
"@fortawesome/angular-fontawesome": "0.15.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.1",
|
||||
"@stomp/ng2-stompjs": "^8.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
@ -2925,6 +2927,52 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/angular-fontawesome": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.15.0.tgz",
|
||||
"integrity": "sha512-oxmJDYGNSym5ycFR0LX4ZOPAU+wWmMAznYpkm5DNAtWWkhMLcrZl15eZQmVIEE+qruQ7JiVrg3tpo8bEkFlDgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz",
|
||||
"integrity": "sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz",
|
||||
"integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz",
|
||||
"integrity": "sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/checkbox": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz",
|
||||
|
||||
@ -21,7 +21,9 @@
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.10",
|
||||
"@stomp/ng2-stompjs": "^8.0.0"
|
||||
"@stomp/ng2-stompjs": "^8.0.0",
|
||||
"@fortawesome/angular-fontawesome": "0.15.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.2.11",
|
||||
|
||||
27
src/main/angular/src/app/Area/Area.ts
Normal file
27
src/main/angular/src/app/Area/Area.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {orNull, validateString} from "../api/validators";
|
||||
|
||||
export class Area {
|
||||
|
||||
constructor(
|
||||
readonly parent: Area | null,
|
||||
readonly uuid: string,
|
||||
readonly slug: string,
|
||||
readonly name: string,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): Area {
|
||||
return new Area(
|
||||
orNull(json.parent, Area.fromJson),
|
||||
validateString(json.uuid),
|
||||
validateString(json.slug),
|
||||
validateString(json.name),
|
||||
);
|
||||
}
|
||||
|
||||
static compareByName(a: Area, b: Area): number {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,30 +1,33 @@
|
||||
import {Property} from "../Property/Property";
|
||||
import {orNull, validateString} from "../common/validators";
|
||||
import {orNull, validateList, validateString} from "../api/validators";
|
||||
import {Area} from '../Area/Area';
|
||||
import {Thing} from '../Thing/Thing';
|
||||
import {Tag} from '../Tag/Tag';
|
||||
|
||||
export class Device {
|
||||
export class Device extends Thing {
|
||||
|
||||
constructor(
|
||||
readonly uuid: string,
|
||||
readonly name: string,
|
||||
readonly slug: string,
|
||||
area: Area,
|
||||
uuid: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
tagList: Tag[],
|
||||
readonly statePropertyId: string,
|
||||
readonly stateProperty: Property | null,
|
||||
) {
|
||||
//
|
||||
super(area, uuid, name, slug, tagList);
|
||||
}
|
||||
|
||||
static fromJson(json: any): Device {
|
||||
return new Device(
|
||||
Area.fromJson(json.area),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateString(json.slug),
|
||||
validateList(json.tagList, Tag.fromJson),
|
||||
validateString(json.statePropertyId),
|
||||
orNull(json.stateProperty, Property.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
static trackBy(index: number, device: Device) {
|
||||
return device.uuid;
|
||||
}
|
||||
|
||||
}
|
||||
11
src/main/angular/src/app/Device/DeviceFilter.ts
Normal file
11
src/main/angular/src/app/Device/DeviceFilter.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class DeviceFilter {
|
||||
|
||||
search: string = "";
|
||||
|
||||
stateTrue: boolean = true;
|
||||
|
||||
stateFalse: boolean = true;
|
||||
|
||||
stateNull: boolean = true;
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tileContainer deviceList">
|
||||
|
||||
<app-device-tile [now]="now" [device]="device" *ngFor="let device of sorted(); trackBy: Device.trackBy"></app-device-tile>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -1,17 +1,15 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {NgClass, NgForOf} from '@angular/common';
|
||||
import {Device} from '../../api/Device/Device';
|
||||
import {DeviceService} from '../../api/Device/device.service';
|
||||
import {RelativePipe} from '../../api/common/relative.pipe';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {Device} from '../Device';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {DeviceTileComponent} from '../device-tile/device-tile.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-device-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
NgClass,
|
||||
RelativePipe
|
||||
DeviceTileComponent
|
||||
],
|
||||
templateUrl: './device-list.component.html',
|
||||
styleUrl: './device-list.component.less'
|
||||
@ -25,13 +23,7 @@ export class DeviceListComponent implements OnInit, OnDestroy {
|
||||
protected now: Date = new Date();
|
||||
|
||||
@Input()
|
||||
deviceList: Device[] = [];
|
||||
|
||||
constructor(
|
||||
protected readonly deviceService: DeviceService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
list: Device[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.now = new Date();
|
||||
@ -42,11 +34,8 @@ export class DeviceListComponent implements OnInit, OnDestroy {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
ngClass(device: Device) {
|
||||
return {
|
||||
"stateOn": device.stateProperty?.state?.value === true,
|
||||
"stateOff": device.stateProperty?.state?.value === false,
|
||||
};
|
||||
sorted(): Device[] {
|
||||
return this.list.sort(Device.compareByAreaThenName);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<div class="tile">
|
||||
|
||||
<div class="tileInner device" [ngClass]="ngClass()">
|
||||
|
||||
<div class="name">
|
||||
{{ device.nameWithArea }}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="action switchOn" (click)="setState(true)"></div>
|
||||
<div class="action switchOff" (click)="setState(false)"></div>
|
||||
</div>
|
||||
|
||||
<div class="timestamp details">
|
||||
{{ device.stateProperty?.lastValueChange | relative:now }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1,37 @@
|
||||
@import "../../../config";
|
||||
|
||||
.device {
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
clear: left;
|
||||
float: left;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
|
||||
.action {
|
||||
float: left;
|
||||
margin-left: @space;
|
||||
width: 4em;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.switchOn {
|
||||
//noinspection CssUnknownTarget
|
||||
background-image: url("/switchOn.svg");
|
||||
}
|
||||
|
||||
.switchOff {
|
||||
//noinspection CssUnknownTarget
|
||||
background-image: url("/switchOff.svg");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {RelativePipe} from "../../api/relative.pipe";
|
||||
import {Device} from "../Device";
|
||||
import {DeviceService} from '../device.service';
|
||||
import {NgClass} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-device-tile',
|
||||
standalone: true,
|
||||
imports: [
|
||||
RelativePipe,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './device-tile.component.html',
|
||||
styleUrl: './device-tile.component.less'
|
||||
})
|
||||
export class DeviceTileComponent {
|
||||
|
||||
@Input()
|
||||
now!: Date;
|
||||
|
||||
@Input()
|
||||
device!: Device;
|
||||
|
||||
constructor(
|
||||
protected readonly deviceService: DeviceService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngClass() {
|
||||
return {
|
||||
"stateOn": this.device.stateProperty?.state?.value === true,
|
||||
"stateOff": this.device.stateProperty?.state?.value === false,
|
||||
};
|
||||
}
|
||||
|
||||
setState(newState: boolean) {
|
||||
if (!this.device.hasTagBySlug('confirm') || confirm("Sicher?")) {
|
||||
this.deviceService.setState(this.device, newState);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {CrudService} from '../common/CrudService';
|
||||
import {CrudService} from '../api/CrudService';
|
||||
import {Device} from './Device';
|
||||
import {ApiService} from '../common/api.service';
|
||||
import {Next} from '../common/types';
|
||||
|
||||
import {DeviceFilter} from './DeviceFilter';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {Next} from '../api/types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -21,10 +19,6 @@ export class DeviceService extends CrudService<Device> {
|
||||
this.getSingle(['getByUuid', uuid], next);
|
||||
}
|
||||
|
||||
list(filter: DeviceFilter | null, next: Next<Device[]>): void {
|
||||
this.postList(['list'], filter, next);
|
||||
}
|
||||
|
||||
setState(device: Device, state: boolean, next?: Next<void>): void {
|
||||
this.getNone(['setState', device.uuid, state], next);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {orNull, validateDateOrNull, validateString} from '../common/validators';
|
||||
import {orNull, validateDateOrNull, validateString} from '../api/validators';
|
||||
import {State} from '../State/State';
|
||||
|
||||
export class Group {
|
||||
@ -29,5 +29,9 @@ export class Group {
|
||||
return group.address;
|
||||
}
|
||||
|
||||
static compareByName(a: Group, b: Group): number {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {CrudService} from '../common/CrudService';
|
||||
import {CrudService} from '../api/CrudService';
|
||||
import {Group} from './Group';
|
||||
import {ApiService} from '../common/api.service';
|
||||
import {Next} from '../common/types';
|
||||
|
||||
import {GroupFilter} from './GroupFilter';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {Next} from '../api/types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -21,8 +19,4 @@ export class GroupService extends CrudService<Group> {
|
||||
this.getSingle(['getByAddress', address], next);
|
||||
}
|
||||
|
||||
list(filter: GroupFilter | null, next: Next<Group[]>): void {
|
||||
this.postList(['list'], filter, next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
<div class="flexBox">
|
||||
<div class="flexBoxFixed">
|
||||
<input [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
|
||||
<app-search [(search)]="filter.search" (doSearch)="fetch()"></app-search>
|
||||
</div>
|
||||
<div class="flexBoxRest">
|
||||
<div class="flexBoxRest verticalScroll">
|
||||
<app-knx-group-list [groupList]="groupList"></app-knx-group-list>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,18 +1,20 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {KnxGroupListComponent} from '../../shared/knx-group-list/knx-group-list.component';
|
||||
import {Group} from '../../api/Group/Group';
|
||||
import {GroupService} from '../../api/Group/group.service';
|
||||
import {KnxGroupListComponent} from '../knx-group-list/knx-group-list.component';
|
||||
import {Group} from '../Group';
|
||||
import {GroupService} from '../group.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {GroupFilter} from '../../api/Group/GroupFilter';
|
||||
import {GroupFilter} from '../GroupFilter';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ApiService} from '../../api/common/api.service';
|
||||
import {ApiService} from '../../api/api.service';
|
||||
import {SearchComponent} from '../../shared/search/search.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-knx-group-list-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
KnxGroupListComponent,
|
||||
FormsModule
|
||||
FormsModule,
|
||||
SearchComponent
|
||||
],
|
||||
templateUrl: './knx-group-list-page.component.html',
|
||||
styleUrl: './knx-group-list-page.component.less'
|
||||
@ -25,8 +27,6 @@ export class KnxGroupListPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected filter: GroupFilter = new GroupFilter();
|
||||
|
||||
private fetchTimeout: any;
|
||||
|
||||
constructor(
|
||||
protected readonly groupService: GroupService,
|
||||
protected readonly apiService: ApiService,
|
||||
@ -44,16 +44,8 @@ export class KnxGroupListPageComponent implements OnInit, OnDestroy {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
fetchDelayed() {
|
||||
if (this.fetchTimeout) {
|
||||
clearTimeout(this.fetchTimeout);
|
||||
this.fetchTimeout = undefined;
|
||||
}
|
||||
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
|
||||
}
|
||||
|
||||
private fetch() {
|
||||
this.groupService.list(this.filter, list => this.groupList = list)
|
||||
protected fetch() {
|
||||
this.groupService.filter(this.filter, list => this.groupList = list)
|
||||
}
|
||||
|
||||
private updateGroup(group: Group) {
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tileContainer groupList">
|
||||
|
||||
<app-knx-group-tile [now]="now" [group]="group" *ngFor="let group of sorted(); trackBy: Group.trackBy"></app-knx-group-tile>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -1,16 +1,15 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {NgClass, NgForOf} from '@angular/common';
|
||||
import {Group} from '../../api/Group/Group';
|
||||
import {RelativePipe} from '../../api/common/relative.pipe';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {Group} from '../Group';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {KnxGroupTileComponent} from '../knx-group-tile/knx-group-tile.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-knx-group-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
NgClass,
|
||||
RelativePipe
|
||||
KnxGroupTileComponent
|
||||
],
|
||||
templateUrl: './knx-group-list.component.html',
|
||||
styleUrl: './knx-group-list.component.less'
|
||||
@ -26,13 +25,6 @@ export class KnxGroupListComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
groupList: Group[] = [];
|
||||
|
||||
ngClass(group: Group) {
|
||||
return {
|
||||
"stateOn": group.state?.value === true,
|
||||
"stateOff": group.state?.value === false,
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.now = new Date();
|
||||
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
|
||||
@ -42,4 +34,8 @@ export class KnxGroupListComponent implements OnInit, OnDestroy {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
sorted(): Group[] {
|
||||
return this.groupList.sort(Group.compareByName);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<div class="tile">
|
||||
|
||||
<div class="tileInner group" [ngClass]="ngClass(group)">
|
||||
|
||||
<div class="name">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
|
||||
<div class="stackLeft address">
|
||||
{{ group.address }}
|
||||
</div>
|
||||
|
||||
<div class="stackLeft dpt">
|
||||
DPT {{ group.dpt }}
|
||||
</div>
|
||||
|
||||
<div class="stackRight state">
|
||||
{{ group.state?.string || '-' }}
|
||||
</div>
|
||||
|
||||
<div class="stackRight timestamp">
|
||||
{{ group.lastValueChange | relative:now }}:
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1,8 @@
|
||||
@import "../../../config";
|
||||
|
||||
.group {
|
||||
|
||||
.name {
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {NgClass} from '@angular/common';
|
||||
import {RelativePipe} from '../../api/relative.pipe';
|
||||
import {Group} from '../Group';
|
||||
|
||||
@Component({
|
||||
selector: 'app-knx-group-tile',
|
||||
standalone: true,
|
||||
imports: [
|
||||
RelativePipe,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './knx-group-tile.component.html',
|
||||
styleUrl: './knx-group-tile.component.less'
|
||||
})
|
||||
export class KnxGroupTileComponent {
|
||||
|
||||
protected readonly Group = Group;
|
||||
|
||||
@Input()
|
||||
now!: Date;
|
||||
|
||||
@Input()
|
||||
group!: Group;
|
||||
|
||||
ngClass(group: Group) {
|
||||
return {
|
||||
"stateOn": group.state?.value === true,
|
||||
"stateOff": group.state?.value === false,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import {State} from "../State/State";
|
||||
import {orNull, validateDateOrNull, validateString} from "../common/validators";
|
||||
import {orNull, validateDateOrNull, validateString} from "../api/validators";
|
||||
|
||||
export class Property {
|
||||
|
||||
@ -1,30 +1,34 @@
|
||||
import {Property} from "../Property/Property";
|
||||
import {orNull, validateString} from "../common/validators";
|
||||
import {orNull, validateList, validateString} from "../api/validators";
|
||||
|
||||
export class Shutter {
|
||||
import {Area} from '../Area/Area';
|
||||
import {Thing} from '../Thing/Thing';
|
||||
import {Tag} from '../Tag/Tag';
|
||||
|
||||
export class Shutter extends Thing {
|
||||
|
||||
constructor(
|
||||
readonly uuid: string,
|
||||
readonly name: string,
|
||||
readonly slug: string,
|
||||
area: Area,
|
||||
uuid: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
tagList: Tag[],
|
||||
readonly positionPropertyId: string,
|
||||
readonly positionProperty: Property | null,
|
||||
) {
|
||||
//
|
||||
super(area, uuid, name, slug, tagList);
|
||||
}
|
||||
|
||||
static fromJson(json: any): Shutter {
|
||||
return new Shutter(
|
||||
Area.fromJson(json.area),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateString(json.slug),
|
||||
validateList(json.tagList, Tag.fromJson),
|
||||
validateString(json.positionPropertyId),
|
||||
orNull(json.positionProperty, Property.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
static trackBy(index: number, shutter: Shutter) {
|
||||
return shutter.uuid;
|
||||
}
|
||||
|
||||
}
|
||||
13
src/main/angular/src/app/Shutter/ShutterFilter.ts
Normal file
13
src/main/angular/src/app/Shutter/ShutterFilter.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export class ShutterFilter {
|
||||
|
||||
search: string = "";
|
||||
|
||||
positionOpen: boolean = true;
|
||||
|
||||
positionBetween: boolean = true;
|
||||
|
||||
positionClosed: boolean = true;
|
||||
|
||||
stateNull: boolean = true;
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tileContainer shutterList">
|
||||
|
||||
<app-shutter-tile [now]="now" [shutter]="shutter" *ngFor="let shutter of sorted(); trackBy: Shutter.trackBy"></app-shutter-tile>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -1,18 +1,15 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {Shutter} from '../../api/Shutter/Shutter';
|
||||
import {ShutterService} from '../../api/Shutter/shutter.service';
|
||||
import {RelativePipe} from '../../api/common/relative.pipe';
|
||||
import {Shutter} from '../Shutter';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
|
||||
import {ShutterTileComponent} from '../shutter-tile/shutter-tile.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shutter-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
RelativePipe,
|
||||
ShutterIconComponent
|
||||
ShutterTileComponent
|
||||
],
|
||||
templateUrl: './shutter-list.component.html',
|
||||
styleUrl: './shutter-list.component.less'
|
||||
@ -26,13 +23,7 @@ export class ShutterListComponent implements OnInit, OnDestroy {
|
||||
protected now: Date = new Date();
|
||||
|
||||
@Input()
|
||||
shutterList: Shutter[] = [];
|
||||
|
||||
constructor(
|
||||
protected readonly shutterService: ShutterService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
list: Shutter[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.now = new Date();
|
||||
@ -43,4 +34,8 @@ export class ShutterListComponent implements OnInit, OnDestroy {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
sorted(): Shutter[] {
|
||||
return this.list.sort(Shutter.compareByAreaThenName);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
<div class="window" (click)="activate.emit(position)">
|
||||
<div *ngIf="isSet(position)" class="shutter" [style.height]="position + '%'"></div>
|
||||
<div *ngIf="isUnset(position)" class="unknown">?</div>
|
||||
</div>
|
||||
@ -1,6 +1,7 @@
|
||||
@import "../../../../config";
|
||||
|
||||
.window {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightskyblue;
|
||||
@ -11,4 +12,15 @@
|
||||
border-bottom: @border solid black;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1.25em;
|
||||
color: red;
|
||||
font-size: 260%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,14 +1,23 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
import {isSet, isUnset} from '../../../api/validators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shutter-icon',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
imports: [
|
||||
NgIf
|
||||
],
|
||||
templateUrl: './shutter-icon.component.html',
|
||||
styleUrl: './shutter-icon.component.less'
|
||||
})
|
||||
export class ShutterIconComponent {
|
||||
|
||||
protected readonly isUnset = isUnset;
|
||||
|
||||
protected readonly isSet = isSet;
|
||||
|
||||
@Input()
|
||||
position?: number
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
<div class="tile">
|
||||
|
||||
<div class="tileInner shutter" [class.unknown]="isUnset(shutter.positionProperty?.state?.value)">
|
||||
|
||||
<div class="name">
|
||||
{{ shutter.nameWithArea }}
|
||||
</div>
|
||||
|
||||
<div class="icon">
|
||||
<app-shutter-icon [position]="shutter.positionProperty?.state?.value"></app-shutter-icon>
|
||||
</div>
|
||||
|
||||
<div class="timestamp details">
|
||||
{{ shutter.positionProperty?.lastValueChange | relative:now }}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="action">
|
||||
<app-shutter-icon [position]="0" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||
</div>
|
||||
<div class="action">
|
||||
<app-shutter-icon [position]="50" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||
</div>
|
||||
<div class="action">
|
||||
<app-shutter-icon [position]="80" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||
</div>
|
||||
<div class="action">
|
||||
<app-shutter-icon [position]="90" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||
</div>
|
||||
<div class="action">
|
||||
<app-shutter-icon [position]="100" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1,36 @@
|
||||
@import "../../../config";
|
||||
|
||||
.shutter {
|
||||
background-color: lightgray;
|
||||
border: @border solid gray !important;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.icon {
|
||||
clear: left;
|
||||
float: left;
|
||||
width: 4em;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
float: right;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
clear: right;
|
||||
float: right;
|
||||
|
||||
.action {
|
||||
float: left;
|
||||
margin-left: @space;
|
||||
width: 3em;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {RelativePipe} from "../../api/relative.pipe";
|
||||
import {Shutter} from "../Shutter";
|
||||
import {ShutterService} from '../shutter.service';
|
||||
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
|
||||
import {isUnset} from '../../api/validators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shutter-tile',
|
||||
standalone: true,
|
||||
imports: [
|
||||
RelativePipe,
|
||||
ShutterIconComponent
|
||||
],
|
||||
templateUrl: './shutter-tile.component.html',
|
||||
styleUrl: './shutter-tile.component.less'
|
||||
})
|
||||
export class ShutterTileComponent {
|
||||
|
||||
protected readonly isUnset = isUnset;
|
||||
|
||||
@Input()
|
||||
now!: Date;
|
||||
|
||||
@Input()
|
||||
shutter!: Shutter;
|
||||
|
||||
constructor(
|
||||
protected readonly shutterService: ShutterService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {CrudService} from '../common/CrudService';
|
||||
import {CrudService} from '../api/CrudService';
|
||||
import {Shutter} from './Shutter';
|
||||
import {ApiService} from '../common/api.service';
|
||||
import {Next} from '../common/types';
|
||||
|
||||
import {ShutterFilter} from './ShutterFilter';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {Next} from '../api/types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -21,10 +19,6 @@ export class ShutterService extends CrudService<Shutter> {
|
||||
this.getSingle(['getByUuid', uuid], next);
|
||||
}
|
||||
|
||||
list(filter: ShutterFilter | null, next: Next<Shutter[]>): void {
|
||||
this.postList(['list'], filter, next);
|
||||
}
|
||||
|
||||
setPosition(shutter: Shutter, position: number, next?: Next<void>): void {
|
||||
this.getNone(['setPosition', shutter.uuid, position], next);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {validateDate, validateString} from "../common/validators";
|
||||
import {validateDate, validateString} from "../api/validators";
|
||||
|
||||
export class State {
|
||||
|
||||
21
src/main/angular/src/app/Tag/Tag.ts
Normal file
21
src/main/angular/src/app/Tag/Tag.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {validateString} from '../api/validators';
|
||||
|
||||
export class Tag {
|
||||
|
||||
constructor(
|
||||
readonly uuid: string,
|
||||
readonly slug: string,
|
||||
readonly name: string,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): Tag {
|
||||
return new Tag(
|
||||
validateString(json.uuid),
|
||||
validateString(json.slug),
|
||||
validateString(json.name),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/angular/src/app/Tag/tag.service.ts
Normal file
17
src/main/angular/src/app/Tag/tag.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {CrudService} from '../api/CrudService';
|
||||
import {Tag} from './Tag';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TagService extends CrudService<Tag> {
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
) {
|
||||
super(apiService, ['Tag'], Tag.fromJson);
|
||||
}
|
||||
|
||||
}
|
||||
48
src/main/angular/src/app/Thing/Thing.ts
Normal file
48
src/main/angular/src/app/Thing/Thing.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {Area} from '../Area/Area';
|
||||
import {Tag} from '../Tag/Tag';
|
||||
|
||||
export abstract class Thing {
|
||||
|
||||
protected constructor(
|
||||
readonly area: Area,
|
||||
readonly uuid: string,
|
||||
readonly name: string,
|
||||
readonly slug: string,
|
||||
readonly tagList: Tag[],
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
get nameOrArea(): string {
|
||||
if (this.name === '') {
|
||||
return this.area.name;
|
||||
}
|
||||
return this.name;
|
||||
}
|
||||
|
||||
get nameWithArea(): string {
|
||||
if (this.name === '') {
|
||||
return this.area.name;
|
||||
}
|
||||
return this.area.name + ' ' + this.name;
|
||||
}
|
||||
|
||||
hasTagBySlug(slug: string): boolean {
|
||||
const slugLower = slug.toLowerCase();
|
||||
return this.tagList.some(t => t.slug.toLocaleLowerCase() === slugLower);
|
||||
}
|
||||
|
||||
static trackBy(index: number, thing: Thing) {
|
||||
return thing.uuid;
|
||||
}
|
||||
|
||||
static compareByAreaThenName(a: Thing, b: Thing): number {
|
||||
const area = Area.compareByName(a.area, b.area);
|
||||
if (area !== 0) {
|
||||
return area;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
7
src/main/angular/src/app/Thing/ThingFilter.ts
Normal file
7
src/main/angular/src/app/Thing/ThingFilter.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class ThingFilter {
|
||||
|
||||
tag: string = "";
|
||||
|
||||
search: string = "";
|
||||
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<div class="flexBox">
|
||||
<div class="flexBoxFixed">
|
||||
<app-search [(search)]="filter.search" (doSearch)="liveList.refresh()"></app-search>
|
||||
</div>
|
||||
<div class="flexBoxRest verticalScroll">
|
||||
<app-thing-list [list]="liveList.list"></app-thing-list>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,67 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {ThingListComponent} from '../thing-list/thing-list.component';
|
||||
import {Thing} from '../Thing';
|
||||
import {ThingService} from '../thing.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ThingFilter} from '../ThingFilter';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {CrudLiveList} from '../../api/CrudLiveList';
|
||||
import {SearchComponent} from '../../shared/search/search.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thing-list-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ThingListComponent,
|
||||
FormsModule,
|
||||
SearchComponent
|
||||
],
|
||||
templateUrl: './thing-list-page.component.html',
|
||||
styleUrl: './thing-list-page.component.less'
|
||||
})
|
||||
export class ThingListPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
protected readonly filter: ThingFilter = new ThingFilter();
|
||||
|
||||
protected readonly liveList: CrudLiveList<Thing>;
|
||||
|
||||
private paramsSet: boolean = false;
|
||||
|
||||
constructor(
|
||||
protected readonly thingService: ThingService,
|
||||
protected readonly activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
this.subs.push(this.liveList = new CrudLiveList(
|
||||
this.thingService,
|
||||
false,
|
||||
undefined,
|
||||
next => {
|
||||
if (this.paramsSet) {
|
||||
this.thingService.filter(this.filter, next);
|
||||
} else {
|
||||
next([]);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.activatedRoute.params.subscribe(params => {
|
||||
this.paramsSet = true;
|
||||
if (this.paramsSet) {
|
||||
this.filter.tag = params['tag'] || '';
|
||||
this.liveList.refresh();
|
||||
} else {
|
||||
this.liveList.clear();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tileContainer">
|
||||
|
||||
<app-thing-tile [now]="now" [thing]="thing" *ngFor="let thing of sorted(); trackBy: Thing.trackBy"></app-thing-tile>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -0,0 +1,41 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {ThingTileComponent} from '../thing-tile/thing-tile.component';
|
||||
import {Thing} from '../Thing';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thing-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
ThingTileComponent
|
||||
],
|
||||
templateUrl: './thing-list.component.html',
|
||||
styleUrl: './thing-list.component.less'
|
||||
})
|
||||
export class ThingListComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected readonly Thing = Thing;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
protected now: Date = new Date();
|
||||
|
||||
@Input()
|
||||
list: Thing[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.now = new Date();
|
||||
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
sorted(): Thing[] {
|
||||
return this.list.sort(Thing.compareByAreaThenName);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<app-device-tile *ngIf="isDevice()" [now]="now" [device]="asDevice()"></app-device-tile>
|
||||
|
||||
<app-shutter-tile *ngIf="isShutter()" [now]="now" [shutter]="asShutter()"></app-shutter-tile>
|
||||
|
||||
<app-tunable-tile *ngIf="isTunable()" [now]="now" [tunable]="asTunable()"></app-tunable-tile>
|
||||
@ -0,0 +1,55 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {Device} from '../../Device/Device';
|
||||
import {Tunable} from '../../Tunable/Tunable';
|
||||
import {Shutter} from '../../Shutter/Shutter';
|
||||
import {DeviceTileComponent} from '../../Device/device-tile/device-tile.component';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {ShutterTileComponent} from '../../Shutter/shutter-tile/shutter-tile.component';
|
||||
import {TunableTileComponent} from '../../Tunable/tunable-tile/tunable-tile.component';
|
||||
import {Thing} from '../Thing';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thing-tile',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DeviceTileComponent,
|
||||
NgIf,
|
||||
ShutterTileComponent,
|
||||
TunableTileComponent
|
||||
],
|
||||
templateUrl: './thing-tile.component.html',
|
||||
styleUrl: './thing-tile.component.less'
|
||||
})
|
||||
export class ThingTileComponent {
|
||||
|
||||
@Input()
|
||||
now!: Date;
|
||||
|
||||
@Input()
|
||||
thing!: Thing;
|
||||
|
||||
asDevice(): Device {
|
||||
return this.thing as Device;
|
||||
}
|
||||
|
||||
isDevice(): boolean {
|
||||
return this.thing instanceof Device;
|
||||
}
|
||||
|
||||
asShutter(): Shutter {
|
||||
return this.thing as Shutter;
|
||||
}
|
||||
|
||||
isShutter(): boolean {
|
||||
return this.thing instanceof Shutter;
|
||||
}
|
||||
|
||||
asTunable(): Tunable {
|
||||
return this.thing as Tunable;
|
||||
}
|
||||
|
||||
isTunable(): boolean {
|
||||
return this.thing instanceof Tunable;
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/angular/src/app/Thing/thing.service.ts
Normal file
34
src/main/angular/src/app/Thing/thing.service.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {CrudService} from '../api/CrudService';
|
||||
import {Thing} from './Thing';
|
||||
import {Next} from '../api/types';
|
||||
import {Subject, Subscription} from 'rxjs';
|
||||
import {DeviceService} from '../Device/device.service';
|
||||
import {ShutterService} from '../Shutter/shutter.service';
|
||||
import {TunableService} from '../Tunable/tunable.service';
|
||||
import {thingFromJson} from './thingFromJson';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThingService extends CrudService<Thing> {
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
protected readonly deviceService: DeviceService,
|
||||
protected readonly shutterService: ShutterService,
|
||||
protected readonly tunableService: TunableService,
|
||||
) {
|
||||
super(apiService, ['Thing'], thingFromJson);
|
||||
}
|
||||
|
||||
override subscribe(next: Next<Thing>): Subscription {
|
||||
const subject = new Subject<Thing>();
|
||||
this.deviceService.subscribe(next => subject.next(next));
|
||||
this.shutterService.subscribe(next => subject.next(next));
|
||||
this.tunableService.subscribe(next => subject.next(next));
|
||||
return subject.subscribe(next);
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/angular/src/app/Thing/thingFromJson.ts
Normal file
18
src/main/angular/src/app/Thing/thingFromJson.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {validateAndRemoveDtoSuffix} from "../api/validators";
|
||||
import {Device} from "../Device/Device";
|
||||
import {Shutter} from "../Shutter/Shutter";
|
||||
import {Tunable} from "../Tunable/Tunable";
|
||||
import {Thing} from "./Thing";
|
||||
|
||||
export function thingFromJson(json: any): Thing {
|
||||
const _type_ = validateAndRemoveDtoSuffix(json._type_);
|
||||
switch (_type_) {
|
||||
case 'Device':
|
||||
return Device.fromJson(json.payload);
|
||||
case 'Shutter':
|
||||
return Shutter.fromJson(json.payload);
|
||||
case 'Tunable':
|
||||
return Tunable.fromJson(json.payload);
|
||||
}
|
||||
throw new Error("Type not implemented: " + _type_);
|
||||
}
|
||||
41
src/main/angular/src/app/Tunable/Tunable.ts
Normal file
41
src/main/angular/src/app/Tunable/Tunable.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {Property} from "../Property/Property";
|
||||
import {orNull, validateList, validateString} from "../api/validators";
|
||||
import {Area} from '../Area/Area';
|
||||
import {Thing} from '../Thing/Thing';
|
||||
import {Tag} from '../Tag/Tag';
|
||||
|
||||
export class Tunable extends Thing {
|
||||
|
||||
constructor(
|
||||
area: Area,
|
||||
uuid: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
tagList: Tag[],
|
||||
readonly statePropertyId: string,
|
||||
readonly stateProperty: Property | null,
|
||||
readonly brightnessPropertyId: string,
|
||||
readonly brightnessProperty: Property | null,
|
||||
readonly coldnessPropertyId: string,
|
||||
readonly coldnessProperty: Property | null,
|
||||
) {
|
||||
super(area, uuid, name, slug, tagList);
|
||||
}
|
||||
|
||||
static fromJson(json: any): Tunable {
|
||||
return new Tunable(
|
||||
Area.fromJson(json.area),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateString(json.slug),
|
||||
validateList(json.tagList, Tag.fromJson),
|
||||
validateString(json.statePropertyId),
|
||||
orNull(json.stateProperty, Property.fromJson),
|
||||
validateString(json.brightnessPropertyId),
|
||||
orNull(json.brightnessProperty, Property.fromJson),
|
||||
validateString(json.coldnessPropertyId),
|
||||
orNull(json.coldnessProperty, Property.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
11
src/main/angular/src/app/Tunable/TunableFilter.ts
Normal file
11
src/main/angular/src/app/Tunable/TunableFilter.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class TunableFilter {
|
||||
|
||||
search: string = "";
|
||||
|
||||
stateTrue: boolean = true;
|
||||
|
||||
stateFalse: boolean = true;
|
||||
|
||||
stateNull: boolean = true;
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tileContainer tunableList">
|
||||
|
||||
<app-tunable-tile [now]="now" [tunable]="tunable" *ngFor="let tunable of sorted(); trackBy: Tunable.trackBy"></app-tunable-tile>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -0,0 +1,43 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {Tunable} from '../Tunable';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {TunableTileComponent} from '../tunable-tile/tunable-tile.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tunable-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
FormsModule,
|
||||
TunableTileComponent
|
||||
],
|
||||
templateUrl: './tunable-list.component.html',
|
||||
styleUrl: './tunable-list.component.less'
|
||||
})
|
||||
export class TunableListComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
list: Tunable[] = [];
|
||||
|
||||
protected readonly Tunable = Tunable;
|
||||
|
||||
protected now: Date = new Date();
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.now = new Date();
|
||||
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
sorted(): Tunable[] {
|
||||
return this.list.sort(Tunable.compareByAreaThenName);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
<div class="tile">
|
||||
|
||||
<div class="tileInner tunable" [ngClass]="ngClass(tunable)">
|
||||
|
||||
<div class="name">
|
||||
{{ tunable.nameWithArea }}
|
||||
</div>
|
||||
|
||||
<div class="timestamp details">
|
||||
{{ tunable.stateProperty?.lastValueChange | relative:now }}
|
||||
</div>
|
||||
|
||||
<div class="sliders">
|
||||
<div class="slider" *ngIf="tunable.brightnessPropertyId !== ''">
|
||||
<app-slider [percent]="tunable.brightnessProperty?.state?.value" (onChange)="tunableService.setBrightness(tunable, $event)" [color0]="dark" [color1]="bright" [colorSlider]="brightness"></app-slider>
|
||||
</div>
|
||||
<div class="slider" *ngIf="tunable.coldnessPropertyId !== ''">
|
||||
<app-slider [percent]="tunable.coldnessProperty?.state?.value" (onChange)="tunableService.setColdness(tunable, $event)" [color0]="warm" [color1]="cold"></app-slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="switch switchOn" (click)="tunableService.setState(tunable, true)"></div>
|
||||
<div class="switch switchOff" (click)="tunableService.setState(tunable, false)"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1,59 @@
|
||||
@import "../../../config";
|
||||
|
||||
.tunable {
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
float: right;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.sliders {
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 60%;
|
||||
overflow: visible;
|
||||
padding-top: 0.4em;
|
||||
|
||||
.slider {
|
||||
float: left;
|
||||
clear: left;
|
||||
margin-left: @space;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
|
||||
.switch {
|
||||
float: left;
|
||||
margin-left: @space;
|
||||
width: 4em;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.switchOn {
|
||||
//noinspection CssUnknownTarget
|
||||
background-image: url("/switchOn.svg");
|
||||
}
|
||||
|
||||
.switchOff {
|
||||
//noinspection CssUnknownTarget
|
||||
background-image: url("/switchOff.svg");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {NgClass, NgIf} from "@angular/common";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {RelativePipe} from "../../api/relative.pipe";
|
||||
import {Tunable} from "../Tunable";
|
||||
import {TunableService} from '../tunable.service';
|
||||
import {SliderComponent} from '../../shared/slider/slider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tunable-tile',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
RelativePipe,
|
||||
NgClass,
|
||||
FormsModule,
|
||||
SliderComponent
|
||||
],
|
||||
templateUrl: './tunable-tile.component.html',
|
||||
styleUrl: './tunable-tile.component.less'
|
||||
})
|
||||
export class TunableTileComponent {
|
||||
|
||||
protected readonly Tunable = Tunable;
|
||||
|
||||
@Input()
|
||||
now!: Date;
|
||||
|
||||
@Input()
|
||||
tunable!: Tunable;
|
||||
|
||||
protected readonly brightness = '#FF0000';
|
||||
|
||||
protected readonly dark = '#000000';
|
||||
|
||||
protected readonly bright = '#ffffff';
|
||||
|
||||
protected readonly warm = '#ffe78e';
|
||||
|
||||
protected readonly cold = '#c4eeff';
|
||||
|
||||
constructor(
|
||||
protected readonly tunableService: TunableService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngClass(tunable: Tunable) {
|
||||
return {
|
||||
"stateOn": tunable.stateProperty?.state?.value === true,
|
||||
"stateOff": tunable.stateProperty?.state?.value === false,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/angular/src/app/Tunable/tunable.service.ts
Normal file
34
src/main/angular/src/app/Tunable/tunable.service.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {CrudService} from '../api/CrudService';
|
||||
import {Tunable} from './Tunable';
|
||||
import {ApiService} from '../api/api.service';
|
||||
import {Next} from '../api/types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TunableService extends CrudService<Tunable> {
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
) {
|
||||
super(api, ['Tunable'], Tunable.fromJson);
|
||||
}
|
||||
|
||||
getByUuid(uuid: string, next: Next<Tunable>): void {
|
||||
this.getSingle(['getByUuid', uuid], next);
|
||||
}
|
||||
|
||||
setState(tunable: Tunable, state: boolean, next?: Next<void>): void {
|
||||
this.getNone(['setState', tunable.uuid, state], next);
|
||||
}
|
||||
|
||||
setBrightness(tunable: Tunable, brightness: number, next?: Next<void>): void {
|
||||
this.getNone(['setBrightness', tunable.uuid, brightness], next);
|
||||
}
|
||||
|
||||
setColdness(tunable: Tunable, coldness: number, next?: Next<void>): void {
|
||||
this.getNone(['setColdness', tunable.uuid, coldness], next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import {Expression, expressionFromJson} from '../Expression/Expression';
|
||||
import {validateBoolean, validateString} from '../common/validators';
|
||||
|
||||
export class Automation {
|
||||
|
||||
constructor(
|
||||
readonly uuid: string,
|
||||
readonly name: string,
|
||||
readonly enabled: boolean,
|
||||
readonly condition: Expression,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): Automation {
|
||||
return new Automation(
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateBoolean(json.enabled),
|
||||
expressionFromJson(json.condition),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
62
src/main/angular/src/app/api/CrudLiveList.ts
Normal file
62
src/main/angular/src/app/api/CrudLiveList.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import {CrudService} from "./CrudService";
|
||||
import {Subscription} from "rxjs";
|
||||
import {Next} from './types';
|
||||
|
||||
interface UUID {
|
||||
|
||||
uuid: string;
|
||||
|
||||
}
|
||||
|
||||
export class CrudLiveList<ENTITY extends UUID> extends Subscription {
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
private unfiltered: ENTITY[] = [];
|
||||
|
||||
list: ENTITY[] = [];
|
||||
|
||||
constructor(
|
||||
readonly crudService: CrudService<ENTITY>,
|
||||
readonly allowAppending: boolean,
|
||||
readonly filter: (item: ENTITY) => boolean = _ => true,
|
||||
readonly all: (next: Next<ENTITY[]>) => any = next => this.crudService.list(next),
|
||||
readonly equals: (a: ENTITY, b: ENTITY) => boolean = (a, b) => a.uuid === b.uuid,
|
||||
) {
|
||||
super(() => {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
});
|
||||
this.subs.push(crudService.api.connected(_ => this.refresh()));
|
||||
this.subs.push(crudService.subscribe(item => this.update(item)));
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.all(list => {
|
||||
this.unfiltered = list;
|
||||
this.updateFiltered();
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.unfiltered = [];
|
||||
this.updateFiltered();
|
||||
}
|
||||
|
||||
private update(item: ENTITY) {
|
||||
const index = this.unfiltered.findIndex(i => this.equals(i, item));
|
||||
if (index >= 0) {
|
||||
this.unfiltered[index] = item;
|
||||
} else {
|
||||
if (!this.allowAppending) {
|
||||
return;
|
||||
}
|
||||
this.unfiltered.push(item);
|
||||
}
|
||||
this.updateFiltered();
|
||||
}
|
||||
|
||||
private updateFiltered() {
|
||||
this.list = this.unfiltered.filter(this.filter);
|
||||
}
|
||||
|
||||
}
|
||||
@ -13,6 +13,14 @@ export abstract class CrudService<ENTITY> {
|
||||
//
|
||||
}
|
||||
|
||||
list(next: Next<ENTITY[]>): void {
|
||||
this.getList(['list'], next);
|
||||
}
|
||||
|
||||
filter<FILTER>(filter: FILTER, next: Next<ENTITY[]>): void {
|
||||
this.postList(['list'], filter, next);
|
||||
}
|
||||
|
||||
subscribe(next: Next<ENTITY>): Subscription {
|
||||
return this.api.subscribe([...this.path], this.fromJson, next);
|
||||
}
|
||||
@ -48,5 +56,4 @@ export abstract class CrudService<ENTITY> {
|
||||
protected postPage(path: any[], data: any, next?: Next<Page<ENTITY>>): void {
|
||||
this.api.postPage([...this.path, ...path], data, this.fromJson, next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export class DeviceFilter {
|
||||
|
||||
search: string = "";
|
||||
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import {validateString} from '../common/validators';
|
||||
import {ExpressionLiteralBoolean} from './ExpressionLiteralBoolean';
|
||||
import {ExpressionLiteralNumber} from './ExpressionLiteralNumber';
|
||||
import {ExpressionProperty} from './ExpressionProperty';
|
||||
import {ExpressionUnary} from './ExpressionUnary';
|
||||
import {ExpressionNary} from './ExpressionNary';
|
||||
|
||||
export abstract class Expression {
|
||||
|
||||
protected constructor(
|
||||
readonly _type_: string,
|
||||
readonly uuid: string,
|
||||
readonly name: string,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function expressionFromJson(json: any): Expression {
|
||||
const _type_ = validateString(json._type_);
|
||||
switch (_type_) {
|
||||
case 'ExpressionLiteralBoolean':
|
||||
return ExpressionLiteralBoolean.fromJson(json);
|
||||
case 'ExpressionLiteralNumber':
|
||||
return ExpressionLiteralNumber.fromJson(json);
|
||||
case 'ExpressionProperty':
|
||||
return ExpressionProperty.fromJson(json);
|
||||
case 'ExpressionUnary':
|
||||
return ExpressionUnary.fromJson(json);
|
||||
case 'ExpressionNary':
|
||||
return ExpressionNary.fromJson(json);
|
||||
}
|
||||
throw Error("expression type not implemented: " + _type_);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import {Expression} from "./Expression";
|
||||
|
||||
export abstract class ExpressionLiteral<T> extends Expression {
|
||||
|
||||
protected constructor(
|
||||
_type_: string,
|
||||
uuid: string,
|
||||
name: string,
|
||||
readonly literal: T | null,
|
||||
) {
|
||||
super(_type_, uuid, name);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import {validateBooleanOrNull, validateString} from "../common/validators";
|
||||
|
||||
import {ExpressionLiteral} from './ExpressionLiteral';
|
||||
|
||||
export class ExpressionLiteralBoolean extends ExpressionLiteral<boolean> {
|
||||
|
||||
constructor(
|
||||
_type_: string,
|
||||
uuid: string,
|
||||
name: string,
|
||||
readonly value: boolean | null,
|
||||
) {
|
||||
super(_type_, uuid, name, value);
|
||||
}
|
||||
|
||||
static fromJson(json: any): ExpressionLiteralBoolean {
|
||||
return new ExpressionLiteralBoolean(
|
||||
validateString(json._type_),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateBooleanOrNull(json.value),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import {validateNumberOrNull, validateString} from "../common/validators";
|
||||
|
||||
import {ExpressionLiteral} from './ExpressionLiteral';
|
||||
|
||||
export class ExpressionLiteralNumber extends ExpressionLiteral<number> {
|
||||
|
||||
constructor(
|
||||
_type_: string,
|
||||
uuid: string,
|
||||
name: string,
|
||||
readonly value: number | null,
|
||||
) {
|
||||
super(_type_, uuid, name, value);
|
||||
}
|
||||
|
||||
static fromJson(json: any): ExpressionLiteralNumber {
|
||||
return new ExpressionLiteralNumber(
|
||||
validateString(json._type_),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateNumberOrNull(json.value),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
import {orNull, validateString} from "../common/validators";
|
||||
import {Expression, expressionFromJson} from './Expression';
|
||||
import {ExpressionNaryOperator} from './ExpressionNaryOperator';
|
||||
|
||||
export class ExpressionNary extends Expression {
|
||||
|
||||
constructor(
|
||||
_type_: string,
|
||||
uuid: string,
|
||||
name: string,
|
||||
readonly operator: ExpressionNaryOperator,
|
||||
readonly expression0: Expression | null,
|
||||
readonly expression1: Expression | null,
|
||||
) {
|
||||
super(_type_, uuid, name);
|
||||
}
|
||||
|
||||
static fromJson(json: any): ExpressionNary {
|
||||
return new ExpressionNary(
|
||||
validateString(json._type_),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateString(json.operator) as ExpressionNaryOperator,
|
||||
orNull(json.expression0, j => expressionFromJson(j)),
|
||||
orNull(json.expression1, j => expressionFromJson(j)),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
export enum ExpressionNaryOperator {
|
||||
OR = 'OR',
|
||||
AND = 'AND',
|
||||
XOR = 'XOR',
|
||||
ADD = 'ADD',
|
||||
SUB = 'SUB',
|
||||
MUL = 'MUL',
|
||||
DIV = 'DIV',
|
||||
MOD = 'MOD',
|
||||
POW = 'POW',
|
||||
HYPOT = 'HYPOT',
|
||||
ATAN2 = 'ATAN2',
|
||||
MIN = 'MIN',
|
||||
MAX = 'MAX',
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import {validateString} from "../common/validators";
|
||||
import {Expression} from './Expression';
|
||||
|
||||
export class ExpressionProperty extends Expression {
|
||||
|
||||
constructor(
|
||||
_type_: string,
|
||||
uuid: string,
|
||||
name: string,
|
||||
readonly propertyId: string,
|
||||
) {
|
||||
super(_type_, uuid, name);
|
||||
}
|
||||
|
||||
static fromJson(json: any): ExpressionProperty {
|
||||
return new ExpressionProperty(
|
||||
validateString(json._type_),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateString(json.propertyId),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import {orNull, validateString} from "../common/validators";
|
||||
import {Expression, expressionFromJson} from './Expression';
|
||||
import {ExpressionUnaryOperator} from './ExpressionUnaryOperator';
|
||||
|
||||
export class ExpressionUnary extends Expression {
|
||||
|
||||
constructor(
|
||||
_type_: string,
|
||||
uuid: string,
|
||||
name: string,
|
||||
readonly operator: ExpressionUnaryOperator,
|
||||
readonly expression: Expression | null,
|
||||
) {
|
||||
super(_type_, uuid, name);
|
||||
}
|
||||
|
||||
static fromJson(json: any): ExpressionUnary {
|
||||
return new ExpressionUnary(
|
||||
validateString(json._type_),
|
||||
validateString(json.uuid),
|
||||
validateString(json.name),
|
||||
validateString(json.operator) as ExpressionUnaryOperator,
|
||||
orNull(json.expression, j => expressionFromJson(j)),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
export enum ExpressionUnaryOperator {
|
||||
NOT = 'NOT',
|
||||
NEG = 'NEG',
|
||||
POS = 'POS',
|
||||
FLOOR = 'FLOOR',
|
||||
CEIL = 'CEIL',
|
||||
ROUND = 'ROUND',
|
||||
SQRT = 'SQRT',
|
||||
ABS = 'ABS',
|
||||
COS = 'COS',
|
||||
SIN = 'SIN',
|
||||
TAN = 'TAN',
|
||||
ACOS = 'ACOS',
|
||||
ASIN = 'ASIN',
|
||||
ATAN = 'ATAN',
|
||||
COSH = 'COSH',
|
||||
SINH = 'SINH',
|
||||
TANH = 'TANH',
|
||||
EXP = 'EXP',
|
||||
EXPM1 = 'EXPM1',
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ExpressionLiteralBoolean} from './ExpressionLiteralBoolean';
|
||||
import {ApiService} from '../common/api.service';
|
||||
import {CrudService} from '../common/CrudService';
|
||||
import {Next} from '../common/types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ExpressionLiteralBooleanService extends CrudService<ExpressionLiteralBoolean> {
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
) {
|
||||
super(apiService, ['ExpressionLiteralBoolean'], ExpressionLiteralBoolean.fromJson);
|
||||
}
|
||||
|
||||
setValue(expression: ExpressionLiteralBoolean, value: boolean, next?: Next<ExpressionLiteralBoolean>) {
|
||||
this.postSingle([expression.uuid, 'setValue'], value, next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ExpressionLiteralNumber} from './ExpressionLiteralNumber';
|
||||
import {ApiService} from '../common/api.service';
|
||||
import {CrudService} from '../common/CrudService';
|
||||
import {Next} from '../common/types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ExpressionLiteralNumberService extends CrudService<ExpressionLiteralNumber> {
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
) {
|
||||
super(apiService, ['ExpressionLiteralNumber'], ExpressionLiteralNumber.fromJson);
|
||||
}
|
||||
|
||||
setValue(expression: ExpressionLiteralNumber, value: number, next?: Next<ExpressionLiteralNumber>) {
|
||||
this.postSingle([expression.uuid, 'setValue'], value, next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export class ShutterFilter {
|
||||
|
||||
search: string = "";
|
||||
|
||||
}
|
||||
@ -29,7 +29,6 @@ export class ApiService {
|
||||
}
|
||||
|
||||
subscribe<T>(topic: any[], fromJson: FromJson<T>, next: Next<T>): Subscription {
|
||||
console.info("WEBSOCKET SUBSCRIBE", topic)
|
||||
return this.stompService
|
||||
.subscribe(topic.join("/"))
|
||||
.pipe(
|
||||
@ -1,4 +1,4 @@
|
||||
import {environment} from "../../../environments/environment";
|
||||
import {environment} from "../../environments/environment";
|
||||
|
||||
export type FromJson<T> = (json: any) => T;
|
||||
|
||||
@ -78,3 +78,19 @@ export function orNull<T, R>(item: T | null | undefined, map: (t: T) => R): R |
|
||||
}
|
||||
return map(item);
|
||||
}
|
||||
|
||||
export function validateAndRemoveDtoSuffix(json: any): string {
|
||||
const type = validateString(json);
|
||||
if (!type.endsWith('Dto')) {
|
||||
throw Error("Type name does not end with Dto: " + type);
|
||||
}
|
||||
return type.substring(0, type.length - 3);
|
||||
}
|
||||
|
||||
export function isSet(value: any) {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export function isUnset(value: any) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
@ -10,8 +10,6 @@ export function stompServiceFactory() {
|
||||
reconnect_delay: 2000,
|
||||
headers: {},
|
||||
});
|
||||
stomp.connected$.subscribe(_ => console.info("WEBSOCKET CONNECTED"));
|
||||
stomp.webSocketErrors$.subscribe(_ => console.info("WEBSOCKET DISCONNECTED"));
|
||||
stomp.activate();
|
||||
return stomp;
|
||||
}
|
||||
@ -1,8 +1,13 @@
|
||||
<div class="flexBox">
|
||||
<div class="flexBoxFixed menu">
|
||||
<div class="item itemLeft" routerLink="DeviceList" routerLinkActive="active">Geräte</div>
|
||||
<div class="item itemLeft" routerLink="ShutterList" routerLinkActive="active">Rollläden</div>
|
||||
<div class="item itemRight" routerLink="GroupList" routerLinkActive="active">KNX</div>
|
||||
<div class="item itemLeft" routerLink="Dashboard" routerLinkActive="active">Dash</div>
|
||||
<div class="item itemLeft" routerLink="ThingList/device" routerLinkActive="active">Geräte</div>
|
||||
<div class="item itemLeft" routerLink="ThingList/light" routerLinkActive="active">Licht</div>
|
||||
<div class="item itemLeft" routerLink="ThingList/decoration" routerLinkActive="active">Deko</div>
|
||||
<div class="item itemLeft" routerLink="ThingList/shutter" routerLinkActive="active">Rollladen</div>
|
||||
<div class="item itemRight" routerLink="ThingList" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
|
||||
<fa-icon [icon]="faMagnifyingGlass"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexBoxRest">
|
||||
<router-outlet/>
|
||||
@ -10,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<div id="notConnected" *ngIf="apiService.websocketError">
|
||||
<div>
|
||||
<div class="text">
|
||||
Nicht verbunden
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
border: @space solid red;
|
||||
color: red;
|
||||
|
||||
div {
|
||||
.text {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
font-size: 200%;
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||
import {ApiService} from './api/common/api.service';
|
||||
import {ApiService} from './api/api.service';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, RouterLink, RouterLinkActive, NgIf],
|
||||
imports: [RouterOutlet, RouterLink, RouterLinkActive, NgIf, FontAwesomeModule],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.less'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'angular';
|
||||
|
||||
readonly faMagnifyingGlass = faMagnifyingGlass;
|
||||
|
||||
constructor(
|
||||
protected readonly apiService: ApiService,
|
||||
|
||||
@ -7,7 +7,7 @@ import {registerLocaleData} from '@angular/common';
|
||||
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import {stompServiceFactory} from './api/common/ws';
|
||||
import {stompServiceFactory} from './api/ws';
|
||||
import {StompService} from '@stomp/ng2-stompjs';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {KnxGroupListPageComponent} from './pages/knx-group-list-page/knx-group-list-page.component';
|
||||
import {DeviceListPageComponent} from './pages/device-list-page/device-list-page.component';
|
||||
import {ShutterListPageComponent} from './pages/shutter-list-page/shutter-list-page.component';
|
||||
import {KnxGroupListPageComponent} from './Group/knx-group-list-page/knx-group-list-page.component';
|
||||
import {DashboardComponent} from './dashboard/dashboard.component';
|
||||
import {ThingListPageComponent} from './Thing/thing-list-page/thing-list-page.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: 'DeviceList', component: DeviceListPageComponent},
|
||||
{path: 'ShutterList', component: ShutterListPageComponent},
|
||||
{path: 'Dashboard', component: DashboardComponent},
|
||||
{path: 'GroupList', component: KnxGroupListPageComponent},
|
||||
{path: '**', redirectTo: 'GroupList'},
|
||||
{path: 'ThingList', component: ThingListPageComponent},
|
||||
{path: 'ThingList/:tag', component: ThingListPageComponent},
|
||||
{path: '**', redirectTo: 'Dashboard'},
|
||||
];
|
||||
|
||||
11
src/main/angular/src/app/dashboard/dashboard.component.html
Normal file
11
src/main/angular/src/app/dashboard/dashboard.component.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="flexBox">
|
||||
<div class="flexBoxFixed">
|
||||
<app-search [(search)]="search" (doSearch)="refresh()"></app-search>
|
||||
</div>
|
||||
<div class="flexBoxRest verticalScroll">
|
||||
<app-device-list [list]="deviceList.list"></app-device-list>
|
||||
<app-tunable-list [list]="tunableList.list"></app-tunable-list>
|
||||
<app-shutter-list [list]="shutterList.list"></app-shutter-list>
|
||||
<div class="emptyBox" *ngIf="deviceList.list.length === 0 && tunableList.list.length === 0 && shutterList.list.length === 0">- Nichts -</div>
|
||||
</div>
|
||||
</div>
|
||||
10
src/main/angular/src/app/dashboard/dashboard.component.less
Normal file
10
src/main/angular/src/app/dashboard/dashboard.component.less
Normal file
@ -0,0 +1,10 @@
|
||||
@import "../../config";
|
||||
|
||||
.subheading {
|
||||
font-size: 65%;
|
||||
font-style: italic;
|
||||
padding-top: calc(@space * 2);
|
||||
padding-left: calc(@space * 2);
|
||||
color: gray;
|
||||
white-space: nowrap;
|
||||
}
|
||||
101
src/main/angular/src/app/dashboard/dashboard.component.ts
Normal file
101
src/main/angular/src/app/dashboard/dashboard.component.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {DeviceService} from '../Device/device.service';
|
||||
import {TunableService} from '../Tunable/tunable.service';
|
||||
import {Device} from '../Device/Device';
|
||||
import {Tunable} from '../Tunable/Tunable';
|
||||
import {Shutter} from '../Shutter/Shutter';
|
||||
import {ShutterService} from '../Shutter/shutter.service';
|
||||
import {DeviceListComponent} from '../Device/device-list/device-list.component';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {CrudLiveList} from '../api/CrudLiveList';
|
||||
import {TunableListComponent} from '../Tunable/tunable-list/tunable-list.component';
|
||||
import {ShutterListComponent} from '../Shutter/shutter-list/shutter-list.component';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {SearchComponent} from '../shared/search/search.component';
|
||||
import {DeviceFilter} from '../Device/DeviceFilter';
|
||||
import {TunableFilter} from '../Tunable/TunableFilter';
|
||||
import {ShutterFilter} from '../Shutter/ShutterFilter';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DeviceListComponent,
|
||||
FormsModule,
|
||||
TunableListComponent,
|
||||
ShutterListComponent,
|
||||
NgIf,
|
||||
SearchComponent,
|
||||
|
||||
],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.less'
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected deviceList!: CrudLiveList<Device>;
|
||||
|
||||
protected tunableList!: CrudLiveList<Tunable>;
|
||||
|
||||
protected shutterList!: CrudLiveList<Shutter>;
|
||||
|
||||
protected now: Date = new Date();
|
||||
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
protected shutterSubheading: string = "";
|
||||
|
||||
protected shuttersShouldBeOpen: boolean = false;
|
||||
|
||||
protected search: string = '';
|
||||
|
||||
private readonly deviceFilter: DeviceFilter = new DeviceFilter();
|
||||
|
||||
private readonly shutterFilter: ShutterFilter = new ShutterFilter();
|
||||
|
||||
private readonly tunableFilter: TunableFilter = new TunableFilter();
|
||||
|
||||
constructor(
|
||||
protected readonly deviceService: DeviceService,
|
||||
protected readonly shutterService: ShutterService,
|
||||
protected readonly tunableService: TunableService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.newDate();
|
||||
this.subs.push(timer(5000, 5000).subscribe(() => this.newDate()));
|
||||
this.subs.push(this.deviceList = new CrudLiveList(this.deviceService, true, device => device.stateProperty?.state?.value === true, next => this.deviceService.filter(this.deviceFilter, next)));
|
||||
this.subs.push(this.shutterList = new CrudLiveList(this.shutterService, true, shutter => this.shutterFilter2(shutter), next => this.shutterService.filter(this.shutterFilter, next)));
|
||||
this.subs.push(this.tunableList = new CrudLiveList(this.tunableService, true, tunable => tunable.stateProperty?.state?.value === true, next => this.tunableService.filter(this.tunableFilter, next)));
|
||||
}
|
||||
|
||||
private newDate() {
|
||||
this.now = new Date();
|
||||
this.shuttersShouldBeOpen = this.now.getHours() >= 7 && this.now.getHours() < 16;
|
||||
this.shutterSubheading = this.shuttersShouldBeOpen ? "Geschlossene" : "Offene";
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
private shutterFilter2(shutter: Shutter) {
|
||||
if (this.shuttersShouldBeOpen) {
|
||||
return shutter.positionProperty?.state?.value !== 0;
|
||||
} else {
|
||||
return shutter.positionProperty?.state?.value !== 100;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.deviceFilter.search = this.search;
|
||||
this.deviceList.refresh();
|
||||
this.tunableFilter.search = this.search;
|
||||
this.tunableList.refresh();
|
||||
this.shutterFilter.search = this.search;
|
||||
this.shutterList.refresh();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
<div class="flexBox">
|
||||
<div class="flexBoxFixed">
|
||||
<input [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
|
||||
</div>
|
||||
<div class="flexBoxRest">
|
||||
<app-device-list [deviceList]="deviceList"></app-device-list>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,5 +0,0 @@
|
||||
@import "../../../config";
|
||||
|
||||
input {
|
||||
border-bottom: @border solid lightgray;
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {DeviceListComponent} from '../../shared/device-list/device-list.component';
|
||||
import {Device} from '../../api/Device/Device';
|
||||
import {DeviceService} from '../../api/Device/device.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {DeviceFilter} from '../../api/Device/DeviceFilter';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ApiService} from '../../api/common/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-device-list-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DeviceListComponent,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './device-list-page.component.html',
|
||||
styleUrl: './device-list-page.component.less'
|
||||
})
|
||||
export class DeviceListPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
protected deviceList: Device[] = [];
|
||||
|
||||
protected filter: DeviceFilter = new DeviceFilter();
|
||||
|
||||
private fetchTimeout: any;
|
||||
|
||||
constructor(
|
||||
protected readonly deviceService: DeviceService,
|
||||
protected readonly apiService: ApiService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fetch();
|
||||
this.subs.push(this.deviceService.subscribe(device => this.updateDevice(device)));
|
||||
this.apiService.connected(() => this.fetch());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
fetchDelayed() {
|
||||
if (this.fetchTimeout) {
|
||||
clearTimeout(this.fetchTimeout);
|
||||
this.fetchTimeout = undefined;
|
||||
}
|
||||
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
|
||||
}
|
||||
|
||||
private fetch() {
|
||||
this.deviceService.list(this.filter, list => this.deviceList = list)
|
||||
}
|
||||
|
||||
private updateDevice(device: Device) {
|
||||
const index = this.deviceList.findIndex(d => d.uuid === device.uuid);
|
||||
if (index >= 0) {
|
||||
this.deviceList.splice(index, 1, device);
|
||||
} else {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
<div class="flexBox">
|
||||
<div class="flexBoxFixed">
|
||||
<input [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
|
||||
</div>
|
||||
<div class="flexBoxRest">
|
||||
<app-shutter-list [shutterList]="shutterList"></app-shutter-list>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user