Compare commits

...

23 Commits

Author SHA1 Message Date
63830cd58d SliderComponent code clean 2 2024-11-29 14:40:47 +01:00
0936a7bc47 SliderComponent code clean 2024-11-29 13:58:02 +01:00
116158ab1f SliderComponent: brightness, coldness 2024-11-29 13:22:30 +01:00
3da3dea353 FIX: used wrong CrudAction.CREATE for update 2024-11-29 13:22:30 +01:00
9023f57c37 removed tunable names "Spots" 2024-11-29 13:20:04 +01:00
b3ecb231c8 ThingList trackBy 2024-11-29 13:19:34 +01:00
4fd64acf09 ThingList search menu item + fontAwesome, DemoService slug combine 2024-11-29 10:05:30 +01:00
a9394efc11 UI: tagList, tagConfirm 2024-11-29 09:43:36 +01:00
dc6d19a633 Area.parent, menu 'Deko', DemoService: a lot more 2024-11-29 09:38:48 +01:00
3970a9a142 TagService.publish 2024-11-28 15:32:39 +01:00
ea0aa3e00a Dashboard search 2024-11-28 15:29:24 +01:00
8133080e9c Rename: Taggable -> Thing 2024-11-28 15:21:09 +01:00
b6f3db79e4 UI: package restructure 2024-11-28 14:48:46 +01:00
417bf890a0 tvheadend keller_receiver -> keller_receiver_state 2024-11-28 14:37:35 +01:00
61ffab50ba Tag, Taggable 2024-11-28 14:37:22 +01:00
ad130fc35e extracted list-item-components out of: DeviceList, ShutterList, TunableList, KnxGroupList 2024-11-28 11:09:34 +01:00
120d6fffdc Dashboard .emptyBox 2024-11-28 08:27:40 +01:00
dc3e262010 dashboard subheading padding 2024-11-27 14:35:35 +01:00
97d558e9c9 UI: removed websocket logging 2024-11-27 14:34:01 +01:00
27238d73d0 Area 2024-11-27 14:32:25 +01:00
b94f602b4b CrudList and Filter FIXES 2024-11-27 13:25:52 +01:00
73926b13e6 Dashboard + CrudLiveList 2024-11-27 11:55:30 +01:00
bb2af44542 Tunable 2024-11-27 09:45:19 +01:00
156 changed files with 3247 additions and 636 deletions

44
data/G
View File

@ -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" />

View File

@ -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",

View File

@ -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",

View 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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
export class DeviceFilter {
search: string = "";
stateTrue: boolean = true;
stateFalse: boolean = true;
stateNull: boolean = true;
}

View File

@ -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>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1,8 @@
@import "../../../config";
.group {
.name {
}
}

View File

@ -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,
};
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -0,0 +1,13 @@
export class ShutterFilter {
search: string = "";
positionOpen: boolean = true;
positionBetween: boolean = true;
positionClosed: boolean = true;
stateNull: boolean = true;
}

View File

@ -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>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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,
) {
//
}
}

View File

@ -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);
}

View File

@ -1,4 +1,4 @@
import {validateDate, validateString} from "../common/validators";
import {validateDate, validateString} from "../api/validators";
export class State {

View 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),
);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -0,0 +1,7 @@
export class ThingFilter {
tag: string = "";
search: string = "";
}

View File

@ -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>

View File

@ -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());
}
}

View File

@ -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>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View 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);
}
}

View 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_);
}

View 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),
);
}
}

View File

@ -0,0 +1,11 @@
export class TunableFilter {
search: string = "";
stateTrue: boolean = true;
stateFalse: boolean = true;
stateNull: boolean = true;
}

View File

@ -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>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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");
}
}
}

View File

@ -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,
};
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -1,5 +0,0 @@
export class DeviceFilter {
search: string = "";
}

View File

@ -1,5 +0,0 @@
export class ShutterFilter {
search: string = "";
}

View File

@ -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(

View File

@ -1,4 +1,4 @@
import {environment} from "../../../environments/environment";
import {environment} from "../../environments/environment";
export type FromJson<T> = (json: any) => T;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -34,7 +34,7 @@
border: @space solid red;
color: red;
div {
.text {
text-align: center;
margin: auto;
font-size: 200%;

View File

@ -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,

View File

@ -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);

View File

@ -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'},
];

View 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>

View 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;
}

View 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();
}
}

View File

@ -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>

View File

@ -1,5 +0,0 @@
@import "../../../config";
input {
border-bottom: @border solid lightgray;
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -1,5 +0,0 @@
@import "../../../config";
input {
border-bottom: @border solid lightgray;
}

View File

@ -1,68 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ShutterListComponent} from '../../shared/shutter-list/shutter-list.component';
import {Shutter} from '../../api/Shutter/Shutter';
import {ShutterService} from '../../api/Shutter/shutter.service';
import {FormsModule} from '@angular/forms';
import {ShutterFilter} from '../../api/Shutter/ShutterFilter';
import {Subscription} from 'rxjs';
import {ApiService} from '../../api/common/api.service';
@Component({
selector: 'app-shutter-list-page',
standalone: true,
imports: [
ShutterListComponent,
FormsModule
],
templateUrl: './shutter-list-page.component.html',
styleUrl: './shutter-list-page.component.less'
})
export class ShutterListPageComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected shutterList: Shutter[] = [];
protected filter: ShutterFilter = new ShutterFilter();
private fetchTimeout: any;
constructor(
protected readonly shutterService: ShutterService,
protected readonly apiService: ApiService,
) {
//
}
ngOnInit(): void {
this.fetch();
this.subs.push(this.shutterService.subscribe(shutter => this.updateShutter(shutter)));
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.shutterService.list(this.filter, list => this.shutterList = list)
}
private updateShutter(shutter: Shutter) {
const index = this.shutterList.findIndex(d => d.uuid === shutter.uuid);
if (index >= 0) {
this.shutterList.splice(index, 1, shutter);
} else {
this.fetch();
}
}
}

View File

@ -0,0 +1,9 @@
<div class="all" (click)="toggle()">
<div class="box">
<div class="TRUE" *ngIf="model">{{ labelTrue }}</div>
<div class="FALSE" *ngIf="!model">{{ labelFalse }}</div>
</div>
<div class="label" *ngIf="label">
{{ label }}
</div>
</div>

View File

@ -0,0 +1,15 @@
@import '../../../config';
.all {
white-space: nowrap;
.box {
float: left;
width: 1.5em;
aspect-ratio: 1;
margin-right: @space;
text-align: center;
border: @border solid gray;
}
}

View File

@ -0,0 +1,35 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {NgIf} from '@angular/common';
@Component({
selector: 'app-checkbox',
standalone: true,
imports: [
NgIf
],
templateUrl: './checkbox.component.html',
styleUrl: './checkbox.component.less'
})
export class CheckboxComponent {
@Input()
label: string = '';
@Input()
labelFalse: string = '';
@Input()
labelTrue: string = 'X';
@Input()
model!: boolean;
@Output()
modelChange: EventEmitter<boolean | null> = new EventEmitter();
toggle() {
this.model = !this.model;
this.modelChange.emit(this.model);
}
}

View File

@ -1,24 +0,0 @@
<div class="deviceList tileContainer">
<div class="tile" *ngFor="let device of deviceList; trackBy: Device.trackBy">
<div class="device tileInner" [ngClass]="ngClass(device)">
<div class="name">
{{ device.name }}
</div>
<div class="actions">
<div class="switchOn" (click)="deviceService.setState(device, true)"></div>
<div class="switchOff" (click)="deviceService.setState(device, false)"></div>
</div>
<div class="timestamp details">
{{ device.stateProperty?.lastValueChange | relative:now }}
</div>
</div>
</div>
</div>

View File

@ -1,43 +0,0 @@
@import "../../../config";
.deviceList {
overflow-y: auto;
height: 100%;
.device {
.name {
float: left;
}
.timestamp {
clear: left;
float: left;
font-size: 80%;
}
.actions {
float: right;
div {
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");
}
}
}
}

View File

@ -1,35 +0,0 @@
<div class="groupList tileContainer">
<div class="tile" *ngFor="let group of groupList; trackBy: Group.trackBy">
<div class="group tileInner" [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>
</div>

View File

@ -1,15 +0,0 @@
@import "../../../config";
.groupList {
overflow-y: auto;
height: 100%;
.group {
.name {
margin-bottom: @space;
}
}
}

View File

@ -0,0 +1,3 @@
<div class="box">
<input type="text" [(ngModel)]="search" (ngModelChange)="fetchDelayed()" placeholder="Filter ...">
</div>

View File

@ -0,0 +1,6 @@
@import "../../../config";
.box {
width: 100%;
padding: @space @space 0 @space;
}

View File

@ -0,0 +1,35 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-search',
standalone: true,
imports: [
FormsModule
],
templateUrl: './search.component.html',
styleUrl: './search.component.less'
})
export class SearchComponent {
@Input()
search: string = '';
@Output()
searchChange: EventEmitter<string> = new EventEmitter();
@Output()
doSearch: EventEmitter<string> = new EventEmitter();
private fetchTimeout: any;
fetchDelayed() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
this.fetchTimeout = undefined;
}
this.searchChange.emit(this.search);
this.fetchTimeout = setTimeout(() => this.doSearch.emit(this.search), 300);
}
}

Some files were not shown because too many files have changed in this diff Show More