Thomas Buck 2 роки тому
коміт
dc837fd65c
4 змінених файлів з 246 додано та 0 видалено
  1. 15
    0
      README.md
  2. 146
    0
      lights/index.html
  3. 66
    0
      lights/lights.js
  4. 19
    0
      localtest.py

+ 15
- 0
README.md Переглянути файл

@@ -0,0 +1,15 @@
1
+# Lights Web
2
+
3
+Simple Bootstrap webinterface to control room lights via MQTT.
4
+
5
+Run `localtest.py` and open `http://localhost:8080` to access local test instance.
6
+
7
+## Sources
8
+
9
+ * [Bootstrap 5.2](https://getbootstrap.com/docs/5.2/getting-started/introduction/)
10
+ * [bootstrap-dark-5](https://github.com/vinorodrigues/bootstrap-dark-5/blob/main/docs/bootstrap-dark.md)
11
+ * [MQTT.js](https://github.com/mqttjs/MQTT.js)
12
+ * [MQTT.js Tutorial](https://www.emqx.com/en/blog/mqtt-js-tutorial)
13
+ * [JS Radio Buttons](https://www.javascripttutorial.net/javascript-dom/javascript-radio-button/)
14
+ * [Python webserver](https://stackoverflow.com/a/52531444)
15
+ * [Re-use socket address](https://stackoverflow.com/a/16641793)

+ 146
- 0
lights/index.html Переглянути файл

@@ -0,0 +1,146 @@
1
+<!doctype html>
2
+<html lang="en">
3
+    <head>
4
+        <meta charset="utf-8">
5
+        <meta name="viewport" content="width=device-width, initial-scale=1">
6
+        <meta name="color-scheme" content="dark light">
7
+        <title>Lights Control</title>
8
+        <!--<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">-->
9
+        <link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-dark.min.css" rel="stylesheet">
10
+    </head>
11
+
12
+    <body>
13
+        <div class="container text-center">
14
+            <div class="row">
15
+                <div class="col">
16
+                    <h1>Lights Control</h1>
17
+                </div>
18
+            </div>
19
+
20
+            <nav>
21
+                <div class="nav nav-tabs" id="nav-tab" role="tablist">
22
+                    <button class="nav-link active" id="nav-bathroom-tab" data-bs-toggle="tab" data-bs-target="#nav-bathroom" type="button" role="tab" aria-controls="nav-bathroom" aria-selected="true">
23
+                        Bathroom
24
+                    </button>
25
+                    <button class="nav-link" id="nav-livingroom-tab" data-bs-toggle="tab" data-bs-target="#nav-livingroom" type="button" role="tab" aria-controls="nav-livingroom" aria-selected="false">
26
+                        Livingroom
27
+                    </button>
28
+                </div>
29
+            </nav>
30
+
31
+            <div class="row">
32
+                <div class="col">
33
+                    <hr>
34
+                </div>
35
+            </div>
36
+
37
+            <div class="tab-content" id="nav-tabContent">
38
+                <div class="tab-pane fade show active" id="nav-bathroom" role="tabpanel" aria-labelledby="nav-bathroom-tab" tabindex="0">
39
+                    <div class="row">
40
+                        <div class="col">
41
+                            Lights
42
+                        </div>
43
+                        <div class="col">
44
+                            <div class="btn-group" role="group">
45
+                                <input type="radio" class="btn-check" name="btnradio" id="bathroomlightauto" autocomplete="off">
46
+                                <label class="btn btn-outline-primary" for="bathroomlightauto">
47
+                                    Auto
48
+                                </label>
49
+                                <input type="radio" class="btn-check" name="btnradio" id="bathroomlightbig" autocomplete="off">
50
+                                <label class="btn btn-outline-success" for="bathroomlightbig">
51
+                                    Big
52
+                                </label>
53
+                                <input type="radio" class="btn-check" name="btnradio" id="bathroomlightsmall" autocomplete="off">
54
+                                <label class="btn btn-outline-info" for="bathroomlightsmall">
55
+                                    Small
56
+                                </label>
57
+                                <input type="radio" class="btn-check" name="btnradio" id="bathroomlightoff" autocomplete="off">
58
+                                <label class="btn btn-outline-dark" for="bathroomlightoff">
59
+                                    Off
60
+                                </label>
61
+                            </div>
62
+                        </div>
63
+                    </div>
64
+                </div>
65
+
66
+                <div class="tab-pane fade" id="nav-livingroom" role="tabpanel" aria-labelledby="nav-livingroom-tab" tabindex="0">
67
+                    <div class="row">
68
+                        <div class="col">
69
+                            <p>No controls available yet.</p>
70
+                        </div>
71
+                    </div>
72
+                </div>
73
+            </div>
74
+
75
+            <div class="row">
76
+                <div class="col">
77
+                    <hr>
78
+                </div>
79
+            </div>
80
+
81
+            <div class="row">
82
+                <div class="col">
83
+                    <p>Please wait after opening the page until the buttons change to reflect the current state of the lights. If that doesn't happen after a couple of seconds, there is probably a connection problem.</p>
84
+                </div>
85
+            </div>
86
+            <div class="row">
87
+                <div class="col">
88
+                    <p><b>Please note:</b> this only works when the MQTT broker and the NodeRED logic are working properly. Also not all web browsers support web socket connections to the MQTT broker. Firefox gives problems, so try a Chromium based browser instead.</p>
89
+                </div>
90
+            </div>
91
+        </div>
92
+
93
+        <!--<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>-->
94
+        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"></script>
95
+        <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
96
+        <script src="lights.js"></script>
97
+
98
+        <script>
99
+            const btns = document.querySelectorAll("#bathroomlightauto, #bathroomlightbig, #bathroomlightsmall, #bathroomlightoff")
100
+
101
+            // handle changes to bathroom lights
102
+            subscribeTopic("bathroom/force_light", function (msg) {
103
+                // clear all buttons
104
+                for (const btn of btns) {
105
+                    btn.checked = false
106
+                }
107
+
108
+                // activate proper button
109
+                if (msg == "none") {
110
+                    const btn = document.querySelector("#bathroomlightauto")
111
+                    btn.checked = true
112
+                } else if (msg == "big") {
113
+                    const btn = document.querySelector("#bathroomlightbig")
114
+                    btn.checked = true
115
+                } else if (msg == "small") {
116
+                    const btn = document.querySelector("#bathroomlightsmall")
117
+                    btn.checked = true
118
+                } else if (msg == "off") {
119
+                    const btn = document.querySelector("#bathroomlightoff")
120
+                    btn.checked = true
121
+                } else {
122
+                    console.log("unknown msg " + msg)
123
+                }
124
+            })
125
+
126
+            // set new bathroom light state
127
+            for (const btn of btns) {
128
+                btn.addEventListener('change', function (e) {
129
+                    if (this.checked) {
130
+                        if (this == document.querySelector("#bathroomlightauto")) {
131
+                            setTopic("bathroom/force_light", "none")
132
+                        } else if (this == document.querySelector("#bathroomlightbig")) {
133
+                            setTopic("bathroom/force_light", "big")
134
+                        } else if (this == document.querySelector("#bathroomlightsmall")) {
135
+                            setTopic("bathroom/force_light", "small")
136
+                        } else if (this == document.querySelector("#bathroomlightoff")) {
137
+                            setTopic("bathroom/force_light", "off")
138
+                        } else {
139
+                            console.log("unknown btn value " + this.value)
140
+                        }
141
+                    }
142
+                })
143
+            }
144
+        </script>
145
+    </body>
146
+</html>

+ 66
- 0
lights/lights.js Переглянути файл

@@ -0,0 +1,66 @@
1
+/*
2
+ * The idea is to use retained messages.
3
+ * This way we can keep the state of the lights.
4
+ * Make sure all senders use retained messages!
5
+ * (in here and shell scripts on PC)
6
+ */
7
+
8
+const options = {
9
+    // Clean session
10
+    clean: true,
11
+    connectTimeout: 4000,
12
+    // Auth
13
+    clientId: 'lights-web',
14
+    username: 'USER_HERE',
15
+    password: 'PW_HERE',
16
+}
17
+const callbacks = []
18
+const client  = mqtt.connect('wss://iot.fritz.box:8083', options)
19
+
20
+client.on('connect', function () {
21
+    console.log('MQTT Connected')
22
+})
23
+
24
+client.on('message', function (topic, message) {
25
+    console.log("Rx \"" + topic.toString() + "\": \"" + message.toString() + "\"")
26
+
27
+    for (const cb of callbacks) {
28
+        if (cb.topic == topic) {
29
+            console.log("Routing to Callback")
30
+            cb.callback(message)
31
+        }
32
+    }
33
+})
34
+
35
+/*
36
+function clearSubscriptions() {
37
+    for (const cb of callbacks) {
38
+        client.unsubscribe(cb.topic)
39
+    }
40
+    callbacks = []
41
+}
42
+*/
43
+
44
+function subscribeTopic(topic, callback) {
45
+    console.log("Sub to \"" + topic.toString() + "\"")
46
+
47
+    subOptions = {
48
+        rh: true,
49
+    }
50
+    client.subscribe(topic)
51
+
52
+    callbackObj = {
53
+        topic: topic,
54
+        callback: callback,
55
+    }
56
+    callbacks.push(callbackObj)
57
+}
58
+
59
+function setTopic(topic, message) {
60
+    console.log("Tx \"" + topic.toString() + "\": \"" + message.toString() + "\"")
61
+
62
+    pubOptions = {
63
+        retain: true,
64
+    }
65
+    client.publish(topic, message, pubOptions)
66
+}

+ 19
- 0
localtest.py Переглянути файл

@@ -0,0 +1,19 @@
1
+#!/usr/bin/env python3
2
+# https://stackoverflow.com/a/52531444
3
+
4
+import http.server
5
+import socketserver
6
+
7
+PORT = 8080
8
+DIRECTORY = "lights"
9
+
10
+class Handler(http.server.SimpleHTTPRequestHandler):
11
+    def __init__(self, *args, **kwargs):
12
+        super().__init__(*args, directory=DIRECTORY, **kwargs)
13
+
14
+# https://stackoverflow.com/a/16641793
15
+socketserver.TCPServer.allow_reuse_address = True
16
+
17
+with socketserver.TCPServer(("", PORT), Handler) as httpd:
18
+    print("serving at port", PORT)
19
+    httpd.serve_forever()

Завантаження…
Відмінити
Зберегти