├── .gitignore ├── LICENSE ├── Plugwise-2-web.py ├── Plugwise-2.py ├── README.md ├── autostart-howto ├── PW2py_bootstart.sh ├── README.md ├── plugwise2py-web.conf └── plugwise2py.conf ├── config-default ├── pw-conf.json ├── pw-control.json ├── pw-hostconfig-mqtt.json └── pw-hostconfig.json ├── config ├── cfg-pw2py.html ├── components │ ├── angularjs │ │ ├── angular.min.js │ │ ├── angular.min.js.map │ │ └── version.txt │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap-theme.css.map │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ └── bootstrap.min.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ └── version.txt │ ├── cfg-pw2py.css │ ├── cfg-pw2py.js │ ├── editform.html │ ├── font-awesome │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ └── fontawesome-webfont.woff │ │ └── version.txt │ ├── hot │ │ ├── dist │ │ │ ├── jquery.handsontable.full.css │ │ │ └── jquery.handsontable.full.js │ │ └── lib │ │ │ ├── jquery-1.10.2.min.map │ │ │ └── jquery.min.js │ ├── modal.html │ ├── modal.js │ ├── pw2py.css │ ├── pw2py.js │ ├── reconnecting-websocket │ │ ├── reconnecting-websocket.js │ │ └── reconnecting-websocket.min.js │ ├── switch.css │ └── ui-bootstrap │ │ └── ui-bootstrap-tpls-0.11.0.min.js ├── index.html ├── pw2py.html └── schedules │ ├── carcharging.json │ ├── testsched1.json │ ├── testsched2.json │ └── winter.json ├── devtools ├── Join-2.py └── plugwsie_util.py ├── domoticz ├── .gitignore ├── README.md └── plugwise2py-domoticz.nodered ├── homey └── README.md ├── openhab ├── README.md └── configurations │ ├── items │ └── plugwise2py.items │ ├── rules │ └── plugwise2py.rules │ ├── sitemaps │ └── plugwise2py.sitemap │ └── transform │ ├── pw2py-avgpower-prod.js │ ├── pw2py-avgpower.js │ ├── pw2py-circleonoff.js │ ├── pw2py-cmdswitch.js │ ├── pw2py-energy.js │ ├── pw2py-fullstate.js │ ├── pw2py-monitor-prod.js │ ├── pw2py-monitor.js │ ├── pw2py-online-n.js │ ├── pw2py-power1h.js │ ├── pw2py-power8s-s.js │ ├── pw2py-power8s.js │ ├── pw2py-reqstate.js │ └── pw2py-switch.js ├── plugwise ├── __init__.py ├── api.py ├── exceptions.py └── protocol.py ├── setup.py ├── swutil ├── HTTPWebSocketsHandler.py ├── __init__.py ├── pwmqtt.py └── util.py └── upstart ├── README.md └── plugwise-2-py /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | /nongit 3 | 4 | #misc 5 | get-pip.py 6 | config/*.json 7 | *.pyc 8 | *.log 9 | *.out 10 | *.pem 11 | -------------------------------------------------------------------------------- /Plugwise-2-web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2012,2013,2014,2015,2016,2017,2018,2019,2020 Seven Watt 4 | # 5 | # 6 | # This file is part of Plugwise-2. 7 | # 8 | # Plugwise-2 is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Plugwise-2 is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Plugwise-2 If not, see . 20 | # 21 | 22 | import sys 23 | import time 24 | import logging 25 | import logging.handlers 26 | import string 27 | import cgi 28 | import urllib.parse 29 | import mimetypes 30 | import os 31 | import glob 32 | import json 33 | 34 | import threading 35 | from socketserver import ThreadingMixIn 36 | from http.server import HTTPServer 37 | from http.server import SimpleHTTPRequestHandler 38 | import ssl 39 | from base64 import b64encode 40 | 41 | from swutil import * 42 | from swutil.util import * 43 | from swutil.pwmqtt import * 44 | from swutil.HTTPWebSocketsHandler import HTTPWebSocketsHandler 45 | 46 | #webroot is the config folder in Plugwise-2-py. 47 | #webserver can only serve files from webroot and subfolders. 48 | #the webroot needs to be the current folder 49 | webroot = os.curdir + os.sep + "config" + os.sep 50 | os.chdir(webroot) 51 | cfg = json.load(open("pw-hostconfig.json")) 52 | 53 | #global var 54 | pw_logger = None 55 | logpath = cfg['log_path']+'/' 56 | init_logger(logpath+"pw-web.log", "pw-web") 57 | if 'log_level' in cfg: 58 | if cfg['log_level'].strip().lower() == 'debug': 59 | log_level(logging.DEBUG) 60 | elif cfg['log_level'].strip().lower() == 'info': 61 | log_level(logging.INFO) 62 | elif cfg['log_level'].strip().lower() == 'error': 63 | log_level(logging.ERROR) 64 | else: 65 | log_level(logging.INFO) 66 | 67 | info('Number of arguments: %d' % (len(sys.argv),)) 68 | info('Argument List: %s' % (sys.argv,)) 69 | 70 | #setup mqtt (if mosquitto bindings are found) 71 | import queue 72 | import threading 73 | #from pwmqttweb import * 74 | 75 | mqtt = True 76 | try: 77 | import paho.mqtt.client as mosquitto 78 | except: 79 | mqtt = False 80 | 81 | qpub = queue.Queue() 82 | qsub = queue.Queue() 83 | broadcast = [] 84 | last_topics = {} 85 | 86 | #bcmutex = threading.Lock() 87 | mqtt_t = None 88 | if not mqtt: 89 | error("No MQTT python binding installed (mosquitto-python)") 90 | elif 'mqtt_ip' in cfg and 'mqtt_port' in cfg: 91 | #connect to server and start worker thread. 92 | if 'mqtt_user' in cfg and 'mqtt_password' in cfg: 93 | mqttclient = Mqtt_client(cfg['mqtt_ip'], cfg['mqtt_port'], qpub, qsub,"Plugwise-2-web",cfg['mqtt_user'],cfg['mqtt_password']) 94 | else: 95 | mqttclient = Mqtt_client(cfg['mqtt_ip'], cfg['mqtt_port'], qpub, qsub, "Plugwise-2-web") 96 | mqttclient.subscribe("plugwise2py/state/#") 97 | mqtt_t = threading.Thread(target=mqttclient.run) 98 | mqtt_t.setDaemon(True) 99 | mqtt_t.start() 100 | info("MQTT thread started") 101 | else: 102 | error("No MQTT broker and port configured") 103 | mqtt = False 104 | 105 | if len(sys.argv) > 1: 106 | port = int(sys.argv[1]) 107 | else: 108 | port = 8000 109 | 110 | if len(sys.argv) > 2: 111 | secure = str(sys.argv[2]).lower()=="secure" 112 | else: 113 | secure = False 114 | if len(sys.argv) > 3: 115 | credentials = str(sys.argv[3]).encode() 116 | else: 117 | credentials = b"" 118 | 119 | def broadcaster(): 120 | while True: 121 | while not qsub.empty(): 122 | rcv = qsub.get() 123 | topic = rcv[0] 124 | payl = rcv[1] 125 | #debug("mqtt broadcaster: %s %s" % (topic, payl)) 126 | #bcmutex.acquire() 127 | last_topics[topic] = payl 128 | for bq in broadcast: 129 | bq.put(rcv) 130 | #bcmutex.release() 131 | time.sleep(0.1) 132 | 133 | bc_t = threading.Thread(target=broadcaster) 134 | bc_t.setDaemon(True) 135 | bc_t.start() 136 | info("Broadcast thread started") 137 | 138 | 139 | class PW2PYwebHandler(HTTPWebSocketsHandler): 140 | def log_message(self, format, *args): 141 | if not args: 142 | debug(self.address_string()+' '+format) 143 | else: 144 | debug(self.address_string()+' '+format % args) 145 | 146 | def log_error(self, format, *args): 147 | error(self.address_string()+' '+format % args) 148 | 149 | def log_request(self, code='-', size='-'): 150 | #self.log_message('"%s" %s %s', self.requestline, str(code), str(size)) 151 | info(self.address_string()+' "%s" %s %s' % (self.requestline, str(code), str(size))) 152 | 153 | def end_headers(self): 154 | self.send_header("Strict-Transport-Security", "max-age=63072000; includeSubDomains") 155 | HTTPWebSocketsHandler.end_headers(self) 156 | 157 | def do_GET(self): 158 | self.log_message("PW2PYwebHandler do_GET") 159 | #debug("GET " + self.path) 160 | if self.path in ['', '/', '/index']: 161 | self.path = '/index.html' 162 | #for this specific application this entry point: 163 | if self.path == '/index.html': 164 | self.path = '/pw2py.html' 165 | #parse url 166 | purl = urllib.parse.urlparse(self.path) 167 | path = purl.path 168 | debug("PW2PYwebHandler.do_GET() parsed: " + path) 169 | if path == '/schedules': 170 | #retrieve list of schedules 171 | schedules = [os.path.splitext(os.path.basename(x))[0] for x in glob.glob(os.curdir + os.sep + 'schedules' + os.sep + '*.json')] 172 | #print schedules 173 | self.send_response(200) 174 | self.send_header('Content-type', 'application/json') 175 | self.end_headers() 176 | self.wfile.write(json.dumps(schedules)) 177 | return 178 | # 179 | #only allow certain file types to be retrieved 180 | elif any(path.endswith(x) for x in ('.ws','.html','.js','.css','.png','.jpg','.gif', '.svg', '.ttf', '.woff', '.txt','.map','.json')): 181 | HTTPWebSocketsHandler.do_GET(self) 182 | else: 183 | self.send_error(404,'Plugwise-2-py-web Page not found') 184 | 185 | 186 | def do_POST(self): 187 | self.log_message("PW2PYwebHandler do_POST") 188 | #self.logRequest() 189 | path = self.path 190 | ctype, pdict = cgi.parse_header(self.headers.getheader('content-type')) 191 | 192 | if (ctype == 'application/x-www-form-urlencoded'): 193 | clienttype = "ajax " 194 | else: 195 | clienttype = "angular " 196 | 197 | info("%s POST: Path %s" % (clienttype, path)) 198 | debug("%s POST: Content-type %s" % (clienttype, ctype)) 199 | 200 | if ((ctype == 'application/x-www-form-urlencoded') or (ctype == 'application/json')): 201 | if (path.startswith('/schedules/') and path.endswith('.json')) or path == '/pw-control.json': 202 | #Write a config or schedule JSON file 203 | debug("POST write a config schedule JSON file") 204 | length = int(self.headers.getheader('content-length')) 205 | raw = self.rfile.read(length) 206 | #print raw 207 | if (ctype == 'application/x-www-form-urlencoded'): 208 | postvars = urllib.parse.parse_qs(raw, keep_blank_values=1) 209 | 210 | if 'data' not in postvars: 211 | 212 | debug("ajaxserver.POST: missing input parameter: %s" % (postvars,)) 213 | self.send_response(404, "data invalid format") 214 | self.send_header('Content-type', 'application/json') 215 | self.end_headers() 216 | self.wfile.write(json.dumps({ "error": "missing data parameter" })) 217 | return 218 | #TODO: check for ajax POST client whether postvars['data'][0] is already the string to write! 219 | #print postvars['data'][0] 220 | ndata = json.loads(postvars['data'][0]) 221 | data = json.dumps(data); 222 | else: 223 | data = raw 224 | #save schedule json file 225 | fnsched = os.curdir + self.path 226 | 227 | debug("POST save schedule to file path: " + fnsched) 228 | try: 229 | with open(fnsched, 'w') as outfile: 230 | #json.dump(data, outfile) 231 | outfile.write(data) 232 | except IOError as err: 233 | 234 | error("POST exception during saving schedule: %s" % (err,)) 235 | self.send_error(404,'Unable to store: %s' % fnsched) 236 | return 237 | 238 | self.send_response(200, "ok") 239 | self.send_header('Content-type', 'application/json') 240 | self.end_headers() 241 | self.wfile.write(json.dumps({ "result": "ok"})) 242 | return 243 | elif path == '/schedules': 244 | length = int(self.headers.getheader('content-length')) 245 | raw = self.rfile.read(length) 246 | debug("POST delete schedule %s" % (json.loads(raw)['delete'],)) 247 | 248 | try: 249 | fnsched = path[1:]+"/"+json.loads(raw)['delete'] 250 | os.remove(fnsched) 251 | except OSError: 252 | self.send_error(404,'Unable to remove: %s' % fnsched) 253 | return 254 | self.send_response(200, "ok") 255 | self.send_header('Content-type', 'application/json') 256 | self.end_headers() 257 | self.wfile.write(json.dumps({ "result": "ok"})) 258 | return 259 | elif path == '/mqtt/': 260 | length = int(self.headers.getheader('content-length')) 261 | raw = self.rfile.read(length) 262 | data = json.loads(raw) 263 | info("POST mqtt message: %s" % data) 264 | topic = str(data['topic']) 265 | msg = json.dumps(data['payload']) 266 | qpub.put((topic, msg)) 267 | self.send_response(200, "ok") 268 | self.send_header('Content-type', 'application/json') 269 | self.end_headers() 270 | self.wfile.write(json.dumps({ "result": "ok"})) 271 | return 272 | 273 | info("POST unhandled. Send 404.") 274 | self.send_response(404, "unsupported POST") 275 | self.send_header('Content-type', 'application/json') 276 | self.end_headers() 277 | self.wfile.write(json.dumps({ "error": "only accepting application/json or application/x-www-form-urlencoded" })) 278 | return 279 | 280 | def on_ws_message(self, message): 281 | if message is None: 282 | message = '' 283 | #self.log_message('websocket received "%s"',str(message)) 284 | try: 285 | data = json.loads(message) 286 | topic = str(data['topic']) 287 | payload = json.dumps(data['payload']) 288 | qpub.put((topic, payload)) 289 | except Exception as err: 290 | self.log_error("Error parsing incoming websocket message: %s", err) 291 | 292 | def on_ws_connected(self): 293 | self.q = queue.Queue() 294 | 295 | 296 | 297 | self.t = threading.Thread(target=self.process_mqtt) 298 | self.t.daemon = True 299 | self.t.start() 300 | 301 | #bcmutex.acquire() 302 | broadcast.append(self.q) 303 | #bcmutex.release() 304 | self.log_message("process_mqtt running on worker thread %d" % self.t.ident) 305 | 306 | def on_ws_closed(self): 307 | #bcmutex.acquire() 308 | broadcast.remove(self.q) 309 | #bcmutex.release() 310 | #join gives issues. threads seems to be reused, so threads end anyways. 311 | #self.t.join() 312 | #self.log_message("on_ws_closed websocket closed for handler %s" % str(self)) 313 | 314 | def process_mqtt(self): 315 | #allow some time for websockets to become fully operational 316 | time.sleep(2.0) 317 | #send last known state to webpage 318 | topics = list(last_topics.items()) 319 | for topic in topics: 320 | self.q.put(topic) 321 | while self.connected: 322 | while not self.q.empty(): 323 | rcv = self.q.get() 324 | topic = rcv[0] 325 | payl = rcv[1] 326 | info("process_mqtt_commands: %s %s" % (topic, payl)) 327 | if self.connected: 328 | self.send_message(payl) 329 | time.sleep(0.5) 330 | 331 | self.log_message("process_mqtt exiting worker thread %d" % self.t.ident) 332 | 333 | 334 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 335 | """Handle requests in a separate thread.""" 336 | 337 | def main(): 338 | try: 339 | server = ThreadedHTTPServer(('', port), PW2PYwebHandler) 340 | server.daemon_threads = True 341 | server.auth = b64encode(credentials) 342 | if secure: 343 | if sys.hexversion < 0x02071000: 344 | #server.socket = ssl.wrap_socket (server.socket, certfile='./server.pem', server_side=True, ssl_version=ssl.PROTOCOL_TLSv1_2) 345 | server.socket = ssl.wrap_socket (server.socket, certfile='./server.pem', server_side=True, ssl_version=ssl.PROTOCOL_TLSv1) 346 | else: 347 | ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 348 | ctx.load_cert_chain(certfile="./server.pem") 349 | ctx.options |= ssl.OP_NO_TLSv1 350 | ctx.options |= ssl.OP_NO_TLSv1_1 351 | ctx.options |= ssl.OP_CIPHER_SERVER_PREFERENCE 352 | ctx.set_ciphers('ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES256-SHA') 353 | server.socket = ctx.wrap_socket(server.socket, server_side=True) 354 | 355 | info('started secure https server at port %d' % (port,)) 356 | else: 357 | info('started http server at port %d' % (port,)) 358 | server.serve_forever() 359 | except KeyboardInterrupt: 360 | print('^C received, shutting down server') 361 | server.shutdown() 362 | print("exit after server.shutdown()") 363 | 364 | if __name__ == '__main__': 365 | main() 366 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Plugwise-2-py 2 | ============= 3 | v2.0 runs on Python3. Python 2.7 is no longer supported, but can be found as v1.1 4 | ------------- 5 | Please submit an issue when stability issues occur. 6 | 7 | 8 | #Key features: 9 | - Web-interface to switch, configure and edit schedules and stand-by killer 10 | - MQTT interface for control and log meter readings. 11 | - Log every 10-seconds in monitoring mode. 12 | - Read and log Circle metering buffers. 13 | - Change interval of Circle metering buffers, e.g. every 2 minutes. 14 | - Enable production metering for e.g. PV solar energy. 15 | - Always-on option, cannot be overridden by switch command. 16 | - Robust matching of commands and replies in Zigbee communication. 17 | - Openhab interface through MQTT. 18 | - Domoticz interface through MQTT. 19 | - Homey interface through MQTT. 20 | - Home Assistant interface through MQTT. 21 | 22 | ##Introduction 23 | Plugwise-2-py evolved in a monitoring and control server for plugwise devices. 24 | In v2.0 it can connect to a MQTT server. Commands to for example switch on a light can be given through the MQTT server, and when enabled, power readings are published as MQTT topics. 25 | Plugwise-2.py is a program is a logger of recorded meterings by plugwise. 26 | It also serves as a controller of the switches, and it can be used to upload 27 | switching/standby schedules to the circles. 28 | 29 | The interface to control is a file interface. There are three configuration files: 30 | - pw-hostconfig.json: some host/server specific settings. 31 | - pw-conf.json: intended as static configuration of the plugs. 32 | - pw-control.json: dynamic configuration. 33 | 34 | Switching / stand-by killer schedules are defines as json files in the schedules folder 35 | - schedules/*.json: contains a week-schedule to switch the plugs on and off. 36 | 37 | Changes to pw-control.json and schedules/*.json are automatically picked up by Plugwise-2.py and applied. 38 | 39 | pw-control.json and schedules/*.json can be edited with the web application (see below) 40 | 41 | In the dynamic configuration: 42 | - logging of the in-circle integrated values can be enabled (usually the one-value-per-hour loggings. 43 | - logging of the actual power (production and or consumption) can be logged. This value will be logged every 10 seconds. 44 | 45 | Finally the code implements several commands that have not been published before, at least not in 2012. 46 | 47 | Besides the new functions for logging and control purposes, there are major improvements in the robustness of the communication. Return status is checked and acted upon correctly. Replies are now correlated to messages, so that no mix-up occur. 48 | 49 | 50 | Setup 51 | ----- 52 | 53 | ```shell 54 | wget https://bootstrap.pypa.io/get-pip.py 55 | sudo python get-pip.py 56 | 57 | git clone https://github.com/SevenW/Plugwise-2-py.git 58 | cd Plugwise-2-py 59 | sudo pip install . 60 | ``` 61 | > Note: include the period "." in the line above! 62 | 63 | ##configuration: 64 | *First time install* 65 | 66 | Template config files are provided in the `config-default` folder. Those can be copied to the `config` folder. Be careful not to overwrite earlier settings. 67 | 68 | ```shell 69 | #from Plugwise-2-py folder: 70 | cp -n config-default/pw-hostconfig.json config/ 71 | cp -n config-default/pw-control.json config/ 72 | cp -n config-default/pw-conf.json config/ 73 | ``` 74 | 75 | *configuring server and circles* 76 | 77 | In config/pw-hostconfig.json edit tmp_path, permanent_path, log_path and serial 78 | 79 | ```{"permanent_path": "/home/pi/datalog", "tmp_path": "/tmp", "log_path": "/home/pi/pwlog", "serial": "/dev/ttyUSB0"}``` 80 | 81 | > Note: For INFO/DEBUG logging, normally /var/log can be used as path. However, on the raspberry pi, only the root user can write in /var/log. Therefore it is better to log to /home//pwlog 82 | 83 | > Note: Editing JSON files is error-prone. Use a JSON Validator such as http://jsonlint.com/ to check the config files. 84 | 85 | Edit the proper Circle mac addresses in pw-conf.json and pw-control.json. Make more changes as appropriate to your needs. 86 | - Enable 10-seconds monitoring: `"monitor": "yes"` 87 | - Enable logging form Circle internal metering buffers: `"savelog": "yes"` 88 | 89 | Note: At first start-up, it starts reading the Circle internal metering buffers form position zero up to the current time. Worst case it can read for three years worth of readings. This may take form several minutes to several hours. 90 | Monitor this activity by tailing the log file: 91 | 92 | `tail -f /home/pi/pwlog/pw-logger.log` 93 | 94 | MQTT can be enable by adding two key,values to pw-hostconfig.json 95 | 96 | `"mqtt_ip": "127.0.0.1", "mqtt_port": "1883"` 97 | 98 | An example config file can be found in pw-hostconfig-mqtt.json 99 | 100 | Plugwise-2-py provides a MQTT-client. A separate MQTT-server like Mosquitto needs to be installed to enable MQTT in Plugwise-2-py. On Ubuntu systems it can be done like this: 101 | 102 | `sudo apt-get install mosquitto` 103 | 104 | The default port is 1883. 105 | 106 | ##run: 107 | 108 | ```nohup python Plugwise-2.py >>/tmp/pwout.log&``` 109 | 110 | ##autostart: 111 | Plugwise-2-py and the web-server can be automatically started with upstart in Ubuntu, or a cron job on the Raspberry pi. See instructions in the `autostart-howto` folder. 112 | 113 | ##debug: 114 | the log level can be programmed in pw-control.json. Changes are picked up latest after 10 seconds. 115 | 116 | `"log_level": "info"` can have values error, info, debug 117 | 118 | `"log_comm": "no"` can have values no and yes. 119 | 120 | log_comm results in logging to pw-communications.log, in the log folder specified through log_path in pw-hostconfig.json 121 | 122 | Update from github 123 | ------------------ 124 | ```shell 125 | #from Plugwise-2-py folder: 126 | git pull 127 | sudo pip install --upgrade . 128 | ``` 129 | 130 | Web interface 131 | ------------- 132 | Plugwise-2-py can be operated through a web interfaces. The packages comes with its own dedicated web-server also written in python. It makes use of websockets for efficient and unsolicited communication. 133 | ##setup 134 | 135 | ```shell 136 | #assume current directory is Plugwise-2-py main directory 137 | nohup python Plugwise-2-web.py 8000 secure plugwise:mysecret >>pwwebout.log& 138 | ``` 139 | 140 | This uses SSL/https. Change plugwise:mysecret in a username:password chosen by yourself. The websserver uses port 8000 by default, and can be changed by an optional parameter: 141 | 142 | `nohup python Plugwise-2-web.py 8001 secure plugwise:mysecret >>pwwebout.log&` 143 | 144 | Providing a user:password is optional, as well as using SSL/https. When the website is only accessible within your LAN, then the server can be used as plain http, by omitting the secure parameter. The following parameter formats are valid: 145 | 146 | ```shell 147 | nohup python Plugwise-2-web.py 8001 secure user:password >>pwwebout.log& 148 | 149 | #no username and password requested 150 | nohup python Plugwise-2-web.py 8000 secure >>pwwebout.log& 151 | 152 | #plain http, with optional port 153 | nohup python Plugwise-2-web.py 8002 >>pwwebout.log& 154 | 155 | #plain http, default port 8000 156 | nohup python Plugwise-2-web.py >>pwwebout.log& 157 | ``` 158 | 159 | ##use 160 | ###Control (switch and monitor) 161 | type in browser 162 | 163 | `https://:8000` 164 | 165 | or 166 | 167 | `https://:8000/pw2py.html` 168 | 169 | for example: 170 | 171 | `http://localhost:8000/pw2py.html` 172 | 173 | in case of SSL/secure: use https//: 174 | 175 | `https://localhost:8000/pw2py.html` 176 | 177 | ###Configure and edit schedules 178 | (No editing static configuration file supported yet) 179 | type in browser: 180 | 181 | `http://:8000/cfg-pw2py.html` 182 | 183 | for example: 184 | 185 | `http://localhost:8000/cfg-pw2py.html` 186 | 187 | 188 | ##require 189 | The Control web-application requires the MQTT connection to be operational. The configuration application can work without MQTT. 190 | 191 | MQTT 192 | ---- 193 | ##power readings 194 | power readings can be published 195 | - autonomous 196 | - on request 197 | 198 | ###Autonomous 199 | Autonomous messages are published when monitoring = "yes" and when savelog = "yes". The 10-seconds monitoring published messages: 200 | 201 | `plugwise2py/state/power/000D6F0001Annnnn {"typ":"pwpower","ts":1405452425,"mac":"000D6F0001Annnnn","power":9.78}` 202 | 203 | The readings of the Circle buffers are published as: 204 | 205 | `plugwise2py/state/energy/000D6F0001Annnnn {"typ":"pwenergy","ts":1405450200,"mac":"000D6F0001nnnnnn","power":214.2069,"energy":35.7012,"interval":10}` 206 | 207 | ###On request 208 | From applications like openhab, a power reading can be requested when needed, or for example scheduled by a cron job. The request will return a full circle state including the short term (8 seconds) integrated power value of the circle. Requests: 209 | 210 | `plugwise2py/cmd/reqstate/000D6F00019nnnnn {"mac":"","cmd":"reqstate","val":"1"}` 211 | 212 | in which val and mac can be an arbitrary values. 213 | The response is published as: 214 | 215 | `plugwise2py/state/circle/000D6F00019nnnnn {"powerts": 1405452834, "name": "circle4", "schedule": "off", "power1s": 107.897, "power8s": 109.218, "readonly": false, "interval": 10, "switch": "on", "mac": "000D6F00019nnnnn", "production": false, "monitor": false, "lastseen": 1405452834, "power1h": 8.228, "online": true, "savelog": true, "type": "circle", "schedname": "test-alternate", "location": "hal1"}` 216 | 217 | ##controlling switches and schedules 218 | Circles can be switched by publishing a command: 219 | 220 | `plugwise2py/cmd/switch/000D6F0001Annnnn {"mac":"","cmd":"switch","val":"on"}` 221 | 222 | or 223 | 224 | `plugwise2py/cmd/schedule/000D6F0001Annnnn {"mac":"","cmd":"schedule","val":"on"}` 225 | 226 | 227 | The circle does respond with a state message, from which it can be deduced whether switching has been successful 228 | 229 | `plugwise2py/state/circle/000D6F0001Annnnn {.... "schedule": "off", "switch": "on" ....}` 230 | 231 | Openhab 232 | ------- 233 | Openhab can communicate with Plugwise-2-py through a MQTT server. Openhab provides a convenient system to operate switches and schedules. Also it can be used to record power readings and draw some graphs. 234 | 235 | TODO: Add a description. 236 | Example sitemap, items, rules and transforms can be found in the openhab folder in this repository 237 | 238 | Domoticz 239 | -------- 240 | A MQTT to HTTP commands interface (using Node-Red) has been developed: 241 | https://www.domoticz.com/forum/viewtopic.php?f=14&t=7420&sid=22502c728a9a4f7d5ac93e6f5c0642a9 242 | 243 | I am investigating a more direct MQTT to Domoticz interface currently. 244 | 245 | Homey 246 | -------- 247 | A Homey app is available in the appstore: https://apps.athom.com/app/com.gruijter.plugwise2py 248 | 249 | For further instructions please visit https://forum.athom.com/discussion/1998 250 | 251 | Home Assistant 252 | -------- 253 | Interfacing with Home Assistant can be done through MQTT. 254 | 255 | Some examples: 256 | ``` 257 | sensor: 258 | - platform: mqtt 259 | name: Coffee 260 | state_topic: 'plugwise2py/state/power/000D6F000XXXXXXX' 261 | unit_of_measurement: 'W' 262 | value_template: '{{ value_json.power }}' 263 | sensor_class: power 264 | ``` 265 | ``` 266 | switch: 267 | - platform: mqtt 268 | name: Coffee 269 | optimistic : false 270 | command_topic: 'plugwise2py/cmd/switch/000D6F000XXXXXXX' 271 | state_topic: 'plugwise2py/state/circle/000D6F000XXXXXXX' 272 | value_template: '{"mac": "000D6F000XXXXXXX", "cmd": "switch", "val": "{{ value_json.switch }}"}' 273 | payload_on: '{"mac": "000D6F000XXXXXXX", "cmd": "switch", "val": "on"}' 274 | payload_off: '{"mac": "000D6F000XXXXXXX", "cmd": "switch", "val": "off"}' 275 | retain: true 276 | ``` 277 | ``` 278 | binary_sensor: 279 | - platform: mqtt 280 | name: 'Plugwise Cicle Status for Coffee' 281 | state_topic: 'plugwise2py/state/circle/000D6F000XXXXXXX' 282 | sensor_class: connectivity 283 | value_template: '{{ value_json.online }}' 284 | payload_on: True 285 | payload_off: False 286 | ``` 287 | 288 | -------------------------------------------------------------------------------- /autostart-howto/PW2py_bootstart.sh: -------------------------------------------------------------------------------- 1 | cd /home/pi/Plugwise-2-py 2 | nohup python Plugwise-2.py >>/tmp/pwout.log& 3 | nohup python Plugwise-2-web.py >>pwwebout.log& 4 | -------------------------------------------------------------------------------- /autostart-howto/README.md: -------------------------------------------------------------------------------- 1 | #Autostart Plugwise-2-py 2 | There are different ways to get Plugwise-2-py and the web-applicaiton automatically started. The first is using the upstart mechanism supported by Ubuntu. The second is using a shell script that is activated by a cron job 3 | 4 | ##1. Upstart 5 | Install the files `plugwise2py.conf` and `plugwise2py-web.conf` in the folder `/etc/init`, by just copying them to that location. 6 | Edit in both files the user credentials as indicated by the comments preceded by "#SET". This concerns linux user name and group under which the program should run. It is not advised to run as root. Also the install path of Plugwise-2-py needs to be correct. Usually this is in the home folder of the user. finally, the location of the python executable needs to be checked. 7 | In the example for Plugwise-2-web.py, the web-server is configured to use a secure connection, so one needs to enter proper user credentials here. 8 | Service can now be started, stopped or restarted with the following commands: 9 | ``` 10 | sudo service plugwise2py start 11 | sudo service plugwise2py stop 12 | sudo service plugwise2py restart 13 | 14 | sudo service plugwise2py-web start 15 | sudo service plugwise2py-web stop 16 | sudo service plugwise2py-web restart 17 | ``` 18 | Both services are automatically started at a (re)boot. 19 | 20 | ##2. Cron job 21 | To make Plugwise-2-py autostart after a system(re)boot you can run a script with cron. Below are the steps for a raspberry pi, where the standard user in Raspbian is 'pi', and where the Plugwise-2-py webinterface is running unsecure without password. 22 | 23 | ``` 24 | cd /home/pi/Plugwise-2-py 25 | nano PW2py_bootstart.sh 26 | # or copy PW2py_bootstart.sh to /home/pi/Plugwise-2-py 27 | # Content of PW2py_bootstart.sh: 28 | # cd /home/pi/Plugwise-2-py 29 | # nohup python Plugwise-2.py >>/tmp/pwout.log& 30 | # nohup python Plugwise-2-web.py >>pwwebout.log& 31 | 32 | # modify the rights so that it really works when cron runs the script! 33 | chmod u+x PW2py_bootstart.sh 34 | 35 | # modify crontab to run the script 36 | crontab -e 37 | # At the end of the crontab file add this line: 38 | # @reboot /home/pi/Plugwise-2-py/PW2py_bootstart.sh 39 | ``` 40 | 41 | ##3. Systemd 42 | Systemd service files for Plugwise can be placed in `/etc/systemd/system`. 43 | In the examples below, plugwise-web.service will start and stop together with plugwise.service using `BindsTo`. 44 | 45 | Replace the references to user `pi` and `/home/pi` below with the respective user/path you are using. 46 | 47 | `plugwise.service`: 48 | ``` 49 | [Unit] 50 | Description=Plugwise 51 | After=network.target 52 | 53 | [Service] 54 | Type=simple 55 | User=pi 56 | ExecStart=/usr/bin/python /home/pi/Plugwise-2-py/Plugwise-2.py 57 | WorkingDirectory=/home/pi/Plugwise-2-py 58 | 59 | [Install] 60 | WantedBy=multi-user.target 61 | ``` 62 | 63 | `plugwise-web.service`: 64 | ``` 65 | [Unit] 66 | Description=Plugwise Web 67 | After=network.target plugwise.service 68 | BindsTo=plugwise.service 69 | 70 | [Service] 71 | Type=simple 72 | User=pi 73 | ExecStart=/usr/bin/python /home/pi/Plugwise-2-py/Plugwise-2-web.py 74 | WorkingDirectory=/home/gerben/Plugwise-2-py 75 | 76 | [Install] 77 | WantedBy=multi-user.target 78 | ``` 79 | 80 | After creating the service files, run `systemctl daemon-reload` and then enable them with `systemctl enable plugwise.service` and `systemctl enable plugwise-web.service`. Starting/stopping is done with `systemctl start plugwise` or `systemctl stop plugwise` 81 | -------------------------------------------------------------------------------- /autostart-howto/plugwise2py-web.conf: -------------------------------------------------------------------------------- 1 | # Plugwise-2-web.py 2 | 3 | description "Plugwise-2-py web interface" 4 | author "SevenW" 5 | 6 | start on (filesystem and stopped udevtrigger) 7 | #start on runlevel [2345] 8 | stop on runlevel [016] 9 | 10 | #SET your linux user and group here by replacing 'user'. Often group can be same as user 11 | setuid user 12 | setgid user 13 | 14 | #expect fork 15 | 16 | respawn 17 | respawn limit 10 3600 18 | 19 | script 20 | #SET path to Plugwise-2-py folder. Most of the time by relpacing 'user' by your linux user name 21 | chdir /home/user/Plugwise-2-py 22 | 23 | #SET webUI login credentials in place of 'WebUser:PassWord' 24 | #SET make sure full path to python is '/usr/bin/python' 25 | exec /usr/bin/python Plugwise-2-web.py 8001 secure 'WebUser:PassWord' 26 | end script 27 | -------------------------------------------------------------------------------- /autostart-howto/plugwise2py.conf: -------------------------------------------------------------------------------- 1 | # Plugwise-2-py 2 | 3 | description "Plugwise-2-py" 4 | author "SevenW" 5 | 6 | start on (filesystem and stopped udevtrigger) 7 | #start on runlevel [2345] 8 | stop on runlevel [016] 9 | 10 | #SET your linux user and group here by replacing 'user'. Often group can be same as user 11 | setuid user 12 | setgid user 13 | 14 | #expect fork 15 | 16 | respawn 17 | respawn limit 10 3600 18 | 19 | script 20 | #SET path to Plugwise-2-py folder. Most of the time by relpacing 'user' by your linux user name 21 | chdir /home/user/Plugwise-2-py 22 | 23 | #SET make sure full path to python is '/usr/bin/python' 24 | exec /usr/bin/python Plugwise-2.py 25 | end script 26 | -------------------------------------------------------------------------------- /config-default/pw-conf.json: -------------------------------------------------------------------------------- 1 | {"static": [ 2 | {"mac": "000D6F0001000000", "category": "misc", "name": "circle+", "loginterval": "60", "always_on": "False", "production": "False", "location": "hall"}, 3 | {"mac": "000D6F0001000001", "category": "misc", "name": "circle1", "loginterval": "60", "always_on": "False", "production": "False", "location": "hall"}, 4 | {"mac": "000D6F0001000002", "category": "misc", "name": "circle2", "loginterval": "60", "always_on": "False", "production": "False", "location": "hall"}, 5 | {"mac": "000D6F0001000003", "category": "PV", "name": "solar-1", "loginterval": "2", "always_on": "True", "production": "True", "location": "attic"}, 6 | {"mac": "000D6F0001000004", "category": "misc", "name": "circle4", "loginterval": "10", "always_on": "False", "production": "False", "location": "hall"}, 7 | {"mac": "000D6F0001000005", "category": "fridge", "name": "circle5", "loginterval": "60", "always_on": "True", "production": "False", "location": "kitchen"}, 8 | {"mac": "000D6F0001000006", "category": "PV", "name": "solar-2", "loginterval": "2", "always_on": "True", "production": "True", "location": "attic"}, 9 | {"mac": "000D6F0001000007", "category": "network", "name": "nas", "loginterval": "60", "always_on": "True", "production": "False", "location": "living"} 10 | ]} 11 | -------------------------------------------------------------------------------- /config-default/pw-control.json: -------------------------------------------------------------------------------- 1 | {"dynamic": [ 2 | {"mac": "000D6F0001000000", "switch_state": "on", "name": "circle+", "schedule_state": "off", "schedule": "", "savelog": "no", "monitor": "no"}, 3 | {"mac": "000D6F0001000001", "switch_state": "on", "name": "circle1", "schedule_state": "off", "schedule": "winter", "savelog": "yes", "monitor": "no"}, 4 | {"mac": "000D6F0001000002", "switch_state": "on", "name": "circle2", "schedule_state": "off", "schedule": "", "savelog": "yes", "monitor": "no"}, 5 | {"mac": "000D6F0001000003", "switch_state": "on", "name": "solar-1", "schedule_state": "off", "schedule": "", "savelog": "yes", "monitor": "yes"}, 6 | {"mac": "000D6F0001000004", "switch_state": "on", "name": "circle4", "schedule_state": "off", "schedule": "__PW2PY__test-alternate", "savelog": "yes", "monitor": "no"}, 7 | {"mac": "000D6F0001000005", "switch_state": "on", "name": "circle5", "schedule_state": "off", "schedule": "", "savelog": "yes", "monitor": "no"}, 8 | {"mac": "000D6F0001000006", "switch_state": "on", "name": "solar-2", "schedule_state": "off", "schedule": "", "savelog": "yes", "monitor": "yes"}, 9 | {"mac": "000D6F0001000007", "switch_state": "on", "name": "nas", "schedule_state": "off", "schedule": "", "savelog": "yes", "monitor": "yes"} 10 | ], 11 | "log_level": "info", 12 | "log_comm": "no"} 13 | -------------------------------------------------------------------------------- /config-default/pw-hostconfig-mqtt.json: -------------------------------------------------------------------------------- 1 | {"permanent_path": "/home/pi/datalog", "tmp_path": "/tmp", "log_path": "/home/pi/pwlog", "serial": "/dev/ttyUSB0", "log_format": "epoch", "mqtt_ip": "127.0.0.1", "mqtt_port": "1883"} -------------------------------------------------------------------------------- /config-default/pw-hostconfig.json: -------------------------------------------------------------------------------- 1 | {"permanent_path": "/home/pi/datalog", "tmp_path": "/tmp", "log_path": "/home/pi/pwlog", "serial": "/dev/ttyUSB0", "log_format": "epoch"} -------------------------------------------------------------------------------- /config/cfg-pw2py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | Plugwise-2.py schedule editor 28 | 29 | 30 |
31 | 32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |

Schedule names: A-Z, a-z, 0-9, _, -, .

42 |
43 | 44 | 45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 90 | 95 | 96 | 97 |
#MACNameLocationCategoryEditRemove
{{$index+1}}{{circle.mac}}{{circle.name}}{{circle.location}}{{circle.category}} 86 |
87 | 88 |
89 |
91 |
92 | 93 |
94 |
98 |
99 | 100 | 101 |
102 |
103 |
104 | 105 |
106 |
107 | 108 |
109 |

Schedule names: A-Z, a-z, 0-9, _, -, .

110 |
111 | 112 | 113 | 114 |
115 | 116 |
117 |
118 |
119 | 120 | 121 | 122 | 123 | 124 | 127 | 128 |
129 |
130 |
131 |
132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 150 | 155 | 156 | 157 |
#nameEditRemove
{{$index+1}}{{row}} 148 | 149 | 151 |
152 | 153 |
154 |
158 |
159 | 160 | 161 |
162 |
163 | 164 |

{{curSched}}

165 |
166 |
167 | 168 |
169 | 170 |
171 |
172 | 175 |
176 |
177 | 180 |
181 |
182 | 185 |
186 |
187 | 190 |
191 |
192 |
193 | 194 | 195 | 196 | {{alrt.msg}} 197 | 198 |
199 | 200 | 201 | 202 | 203 |

Expect some future extensions of the Plugwise-2.py web applicaiton.

204 |

Change log-level

205 |

Enable communication logging

206 |

Enable changes of static ocnfiguration, requiring restart of Plugwise server

207 |

Configure MQTT, etc...

208 | 209 | 212 |
213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /config/components/angularjs/version.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/components/bootstrap/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /config/components/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /config/components/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /config/components/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /config/components/bootstrap/version.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/components/cfg-pw2py.css: -------------------------------------------------------------------------------- 1 | body{ 2 | padding:20px; 3 | } 4 | .search{ 5 | margin-left:10px; 6 | } 7 | 8 | /*Cardview*/ 9 | .cardContainer { 10 | /*width:85%;*/ 11 | } 12 | 13 | .card { 14 | background-color:#fff; 15 | border: 1px solid #d4d4d4; 16 | /*height:300px;*/ 17 | margin-bottom: 20px; 18 | /*position: relative;*/ 19 | border-radius: 10px 10px 10px 10px; 20 | font-size:12pt; 21 | } 22 | 23 | .cardHeader { 24 | background-color: #5bc0de; 25 | /*background-color:#027FF4;*/ 26 | font-size:14pt; 27 | color:white; 28 | padding:5px; 29 | /*width:100%;*/ 30 | border-radius: 10px 10px 0px 0px; 31 | 32 | } 33 | 34 | .cardClose { 35 | color: white; 36 | font-weight:bold; 37 | margin-right:5px; 38 | } 39 | 40 | 41 | .cardBody { 42 | padding-left: 5px; 43 | } 44 | 45 | .cardBodyLeft { 46 | /*margin-top: 20px;*/ 47 | } 48 | 49 | .cardBodyRight { 50 | /*margin-left: 20px;*/ 51 | margin-top: 2px; 52 | } 53 | 54 | .cardBodyContent { 55 | /*width: 100px;*/ 56 | /*margin-left: 20px;*/ 57 | /*margin-top: 20px;*/ 58 | 59 | } 60 | 61 | .cardImage { 62 | /*height:50px;width:50px;margin-top:20px;margin-left:20px;*/ 63 | /*min-width:100%;*/ 64 | min-height:75%; 65 | /*height: 100%;*/ 66 | /*width: 100%;*/ 67 | } 68 | 69 | .cardIcon { 70 | padding-left: 0px; 71 | padding-right: 0px; 72 | } 73 | 74 | .cardReading { 75 | font-size: 22pt; 76 | font-weight:bold; 77 | } 78 | .cardInfo { 79 | font-size: 8pt; 80 | color: #bbb; 81 | } 82 | 83 | .production { 84 | color: green; 85 | } 86 | .vsize50 { 87 | height: 50px; 88 | } 89 | .vsize100 { 90 | height: 100px; 91 | } 92 | .vflex1 { 93 | display: -webkit-flex; 94 | display: flex; 95 | -webkit-flex-direction: row /* works with row or column */ 96 | flex-direction: row; 97 | -webkit-align-items: center; 98 | align-items: center; 99 | -webkit-justify-content: center; 100 | justify-content: center; 101 | } 102 | 103 | .vcenter { 104 | display: -webkit-box; 105 | -webkit-box-orient: horizontal; 106 | -webkit-box-pack: center; 107 | -webkit-box-align: center; 108 | 109 | display: -moz-box; 110 | -moz-box-orient: horizontal; 111 | -moz-box-pack: center; 112 | -moz-box-align: center; 113 | 114 | display: box; 115 | box-orient: horizontal; 116 | box-pack: center; 117 | box-align: center; 118 | } 119 | 120 | .vcenter1{ 121 | /*margin: 0;*/ 122 | position: absolute; 123 | top: 50%; 124 | transform: translate(0, -50%) 125 | } 126 | 127 | .vcenter2 { 128 | display: inline-block; 129 | vertical-align: middle; 130 | float: none; 131 | } 132 | 133 | .vfull { 134 | height: 100%; 135 | } 136 | 137 | /*fix tooltip width bootstrap*/ 138 | .tooltip-inner { 139 | max-width: 350px; 140 | /* If max-width does not work, try using width instead */ 141 | width: 250px; 142 | } 143 | 144 | -------------------------------------------------------------------------------- /config/components/cfg-pw2py.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('schedules', ['ui.bootstrap']); 2 | 3 | var WIDE = true; 4 | 5 | app.controller("mainCtrl", function mainCtrl($scope, $http, $modal){ 6 | $scope.tabs = [true, false, false, false]; 7 | $scope.temp = false; 8 | 9 | $scope.conf = {static: []}; 10 | $scope.config = {dynamic: []}; 11 | $scope.circles = []; 12 | 13 | $scope.curSched = ""; 14 | $scope.rows = []; 15 | $scope.changed = false; 16 | $scope.alrt = { type: 'success', msg: 'ok' }; 17 | $scope.mctrl = {addMAC: "", addName: "", description: "description"} 18 | $scope.mctrl.schedule = (WIDE ? zeroData() : transpose(zeroData())); 19 | $scope.state = {changed: false, fit: true}; 20 | 21 | $scope.loadConfig = function () { 22 | console.log("loadConfig"); 23 | $http.get("/pw-conf.json"). 24 | success(function(data, status, headers, config) { 25 | $scope.conf = data; 26 | $http.get("/pw-control.json"). 27 | success(function(data, status, headers, config) { 28 | $scope.config = data; 29 | //Merge config 30 | for (var i=0; i<$scope.conf.static.length; i++) { 31 | var statconf = $scope.conf.static[i]; 32 | var circle = {}; 33 | for (var key in statconf) { circle[key] = statconf[key]; }; 34 | var dynconf = getByMac($scope.config.dynamic, circle.mac); 35 | if (dynconf != null) { 36 | for (var key in dynconf) { circle[key] = dynconf[key]; }; 37 | } 38 | circle['alwayson'] = (circle.always_on == "True") ? true : false 39 | if (circle['alwayson']) { 40 | circle.switch_state = 'on'; 41 | circle.schedule_state = 'off'; 42 | } 43 | circle['power'] = "-"; 44 | circle['relayon'] = circle.switch_state 45 | 46 | circle.toolTip = "interval: " + circle.loginterval + " min.
" 47 | circle.toolTip += "monitor (10s): " + circle.monitor + "
" 48 | circle.toolTip += "save log (" + circle.loginterval + "m): " + circle.savelog + "
" 49 | circle.toolTip += "mac: " + circle.mac 50 | 51 | circle.icon = "fa-lightbulb-o" 52 | if (circle.category == "PV") { 53 | circle.icon = "fa-bolt" 54 | } else if (circle.category == "divers") { 55 | circle.icon = "fa-plug" 56 | } 57 | 58 | //TESTCODE 59 | circle.alwayson = !(circle.name == 'circle+'); 60 | 61 | 62 | $scope.circles.push(circle); 63 | 64 | //Enrich dynamic config (temporary code) 65 | if (!dynconf.hasOwnProperty('location')) { 66 | dynconf['location'] = statconf.location; 67 | } 68 | if (!dynconf.hasOwnProperty('category')) { 69 | dynconf['category'] = statconf.category; 70 | } 71 | 72 | 73 | console.log(dynconf); 74 | console.log(statconf); 75 | console.log(circle); 76 | } 77 | }). 78 | error(function(data, status, headers, config) { 79 | // log error 80 | }); 81 | }). 82 | error(function(data, status, headers, config) { 83 | // log error 84 | }); 85 | }; 86 | 87 | function getIdxByMac(arr, mac) { 88 | for (var i=0; i 1 ? 's': ''; 133 | }; 134 | 135 | $scope.addTemp = function(){ 136 | if($scope.temp) $scope.rows.pop(); 137 | else if($scope.mctrl.addName) $scope.temp = true; 138 | 139 | if($scope.mctrl.addName) $scope.rows.push($scope.mctrl.addName); 140 | else $scope.temp = false; 141 | }; 142 | 143 | $scope.isTemp = function(i){ 144 | return i==$scope.rows.length-1 && $scope.temp; 145 | }; 146 | 147 | $scope.editRow = function (row) { 148 | $scope.tabs[1] = false; 149 | $scope.tabs[2] = true; 150 | $scope.curSched = row; 151 | console.log("editRow "+row); 152 | $http.get("/schedules/" + row + ".json"). 153 | success(function(data, status, headers, config) { 154 | $scope.raw = data; 155 | $scope.mctrl.description = $scope.raw.description; 156 | $scope.mctrl.schedule = angular.copy(WIDE ? $scope.raw.schedule : transpose($scope.raw.schedule)); 157 | $scope.curInfo = $scope.raw.name; 158 | $scope.alrt = { type: 'success', msg: 'schedule loaded' }; 159 | $scope.state.changed = false; 160 | }). 161 | error(function(data, status, headers, config) { 162 | // log error 163 | }); 164 | }; 165 | 166 | $scope.saveSchedule = function(){ 167 | $scope.raw.schedule = (WIDE ? $scope.mctrl.schedule : transpose($scope.mctrl.schedule)) 168 | $scope.raw.description = $scope.mctrl.description; 169 | $http.post("/schedules/" + $scope.curSched + ".json", $scope.raw). 170 | success(function(data, status, headers, config) { 171 | console.log("scheduled POST successful") 172 | }). 173 | error(function(data, status, headers, config) { 174 | // log error 175 | console.log("scheduled POST error") 176 | }); 177 | 178 | $scope.state.changed = false; 179 | $scope.alrt.msg = 'schedule saved'; 180 | return; 181 | }; 182 | 183 | $scope.cancelSchedule = function(){ 184 | $scope.mctrl.schedule = angular.copy(WIDE ? $scope.raw.schedule : transpose($scope.raw.schedule)); 185 | $scope.mctrl.description = $scope.raw.description; 186 | $scope.state.changed = false; 187 | $scope.alrt.msg = 'reverted to saved'; 188 | return; 189 | }; 190 | 191 | $scope.setChanged = function(){ 192 | $scope.changed = true; 193 | return; 194 | }; 195 | 196 | $scope.toggleWidth = function(){ 197 | console.log("toggleWidth()"); 198 | $scope.state.fit = !$scope.state.fit; 199 | return; 200 | }; 201 | 202 | 203 | function transpose(array) { 204 | var newArray = array[0].map(function(col, i) { 205 | return array.map(function(row) { 206 | return row[i] 207 | }); 208 | }); 209 | return newArray 210 | }; 211 | 212 | function zeroData () { 213 | var matrix = []; 214 | for (var j = 0; j < 7; j++) { 215 | matrix.push(Array.apply(null, new Array(96)).map(Number.prototype.valueOf,0)); 216 | } 217 | return matrix; 218 | } 219 | 220 | function createSchedule (newname) { 221 | var raw = {name: newname, description: "always on"}; 222 | raw.schedule = zeroData(); 223 | return raw; 224 | } 225 | 226 | $scope.editCircle = function (circle) { 227 | var conf = {static: {}, dynamic: {}}; 228 | conf.static = getByMac($scope.conf.static, circle.mac) 229 | conf.dynamic = getByMac($scope.config.dynamic, circle.mac) 230 | var modalInstance = $modal.open({ 231 | templateUrl: 'components/editform.html', 232 | controller: modalInstanceCtrl, 233 | conf: conf, 234 | resolve: { 235 | conf: function() { 236 | return angular.copy(conf); 237 | } 238 | } 239 | }); 240 | 241 | modalInstance.result.then(function (conf) { 242 | //$scope.selected = selectedItem; 243 | $scope.updated_conf = conf; 244 | //sync some fields between static and dynamic conf 245 | $scope.updated_conf.dynamic.mac = $scope.updated_conf.static.mac 246 | $scope.updated_conf.static.name = $scope.updated_conf.dynamic.name 247 | $scope.updated_conf.static.location = $scope.updated_conf.dynamic.location 248 | $scope.updated_conf.static.category = $scope.updated_conf.dynamic.category 249 | 250 | var i_stat = getIdxByMac($scope.conf.static, $scope.updated_conf.static.mac) 251 | var i_dyna = getIdxByMac($scope.config.dynamic, $scope.updated_conf.static.mac) 252 | 253 | $scope.conf.static[i_stat] = $scope.updated_conf.static 254 | $scope.config.dynamic[i_dyna] = $scope.updated_conf.dynamic 255 | 256 | //ONLY save dynamic (for the moment) 257 | $http.post("/pw-control.json", $scope.config). 258 | success(function(data, status, headers, config) { 259 | console.log("pw-control.json POST successful") 260 | }). 261 | error(function(data, status, headers, config) { 262 | // log error 263 | console.log("pw-control.json POST error") 264 | }); 265 | 266 | var i_circ = getIdxByMac($scope.circles, $scope.updated_conf.static.mac) 267 | for (var key in $scope.updated_conf.dynamic) { $scope.circles[i_circ][key] = $scope.updated_conf.dynamic[key]; }; 268 | console.log('save edit of ' + conf.dynamic.name); 269 | 270 | 271 | }, function () { 272 | console.log('cancelled edit of ' + conf.dynamic.name); 273 | }); 274 | }; 275 | 276 | 277 | }) 278 | 279 | app.directive('handsontable', function($window){ 280 | return { 281 | restrict: 'EAC', 282 | scope: { 283 | schedule: '=', 284 | alrt: '=', 285 | unsaved: '=', 286 | fit: '=', 287 | valchanged: '&' 288 | }, 289 | replace: true, 290 | template: '



', 291 | link: function(scope, elem, attrs ){ 292 | Handsontable.renderers.registerRenderer('valueRenderer', valueRenderer); //maps function to lookup string 293 | //$(elem).handsontable({ 294 | elem.handsontable({ 295 | startRows: (WIDE ? 7 : 96), 296 | startCols: (WIDE ? 96 : 7), 297 | maxRows: (WIDE ? 7 : 96), 298 | maxCols: (WIDE ? 96 : 7), 299 | colWidths: 36, 300 | //rowHeights: 36, 301 | rowHeaders: dayheaders, 302 | colHeaders: columnheaders_wide, 303 | cells: function (row, col, prop) { 304 | var cellProperties = {}; 305 | cellProperties.type = 'numeric'; 306 | cellProperties.renderer = "valueRenderer"; //uses lookup map 307 | cellProperties.data = 0; 308 | return cellProperties; 309 | }, 310 | data: scope.schedule, 311 | beforeChange: function (changes, source) { 312 | for (var i = changes.length - 1; i >= 0; i--) { 313 | if (changes[i][3] == null || changes[i][3] === '') { 314 | changes[i][3] = 0; 315 | } else if (isNaN(+changes[i][3])) { 316 | return false; 317 | } else if (changes[i][3] < 0) { 318 | changes[i][3] = -1; 319 | } else if (changes[i][3] > 3000) { 320 | changes[i][3] = -1; //ON in case of very high standby value 321 | } 322 | } 323 | if (source != 'loadData') { 324 | scope.alrt.type = 'success'; 325 | scope.alrt.msg = 'schedule modified'; 326 | scope.unsaved = true; 327 | scope.valchanged(); 328 | scope.$apply(); 329 | } 330 | }, 331 | afterLoadData: function () { 332 | var hot = elem.handsontable('getInstance'); 333 | scope.hot = hot; 334 | for (var r = 0; r < scope.hot.countRows(); r++) { 335 | for (var c = 0; c < scope.hot.countCols(); c++) { 336 | var value = scope.hot.getDataAtCell(r, c); 337 | if (value == null || value === '') { 338 | scope.hot.setDataAtCell(r, c, 0); 339 | } else if (typeof value === 'string') { 340 | scope.hot.setDataAtCell(r, c, parseInt(value)); 341 | } 342 | } 343 | } 344 | }, 345 | 346 | }) 347 | scope.hot = elem.handsontable('getInstance'); 348 | scope.$watch("schedule", function() { 349 | console.log("HOT loadData"); 350 | scope.hot.loadData(scope.schedule); 351 | }); 352 | scope.$watch("fit", function() { 353 | console.log("Toggle fit"); 354 | if (scope.fit) { 355 | //scope.hot.colWidths = 15; 356 | //elem.width = $window.innerWidth; 357 | var w = ($window.innerWidth - 50 ) / 96; 358 | w=Math.max(1, Math.floor(w)); 359 | scope.hot.updateSettings({colWidths: w}); 360 | } else { 361 | //scope.hot.colWidths = 30; 362 | scope.hot.updateSettings({colWidths: 36}); 363 | } 364 | //scope.hot.loadData(scope.schedule); 365 | //scope.hot.render(); 366 | }); 367 | 368 | //local functions for handsontable directive 369 | function columnheaders_wide(index) { 370 | var hour = Math.floor(index /4); 371 | var minute = 15 * (index % 4); 372 | var hour_s = (hour < 10 ? "0" + hour : hour); 373 | var minute_s = (minute < 10 ? "0" + minute : minute); 374 | var time = (hour < 10 ? "0" + hour : hour) + "00"; 375 | j = index % 4; 376 | if (j==0) { 377 | //return ""+(hour < 10 ? "" : time[0])+"
"+time[1]+"
"+time[2]+"
"+time[3]+"
"; 378 | //return '
'+hour_s+'
'; 379 | return '
'+hour_s+'
'; 380 | }; 381 | //return '
'+minute_s+'
'; 382 | return '
'+minute_s+'
'; 383 | }; 384 | function rowheaders_wide(index) { 385 | var hour = Math.floor(index /4); 386 | var minute = 15 * (index % 4); 387 | var time = (hour < 10 ? "0" + hour : hour) + ":" + (minute < 10 ? "0" + minute : minute); 388 | return time; 389 | }; 390 | function dayheaders(index) { 391 | var days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; 392 | return '
'+days[index]+'
'; 393 | }; 394 | function valueRenderer(instance, td, row, col, prop, value, cellProperties) { 395 | Handsontable.renderers.TextRenderer.apply(this, arguments); 396 | if (value == null || value === '') { 397 | td.style.background = 'red'; 398 | } else if (parseInt(value, 10) == 0) { 399 | //td.className = 'schedule-off'; 400 | td.style.background = '#bbb'; 401 | td.style.color = '#bbb' 402 | } else if (parseInt(value, 10) < 0) { 403 | //td.className = 'schedule-on'; 404 | td.style.background = '#f0ad4e' 405 | td.style.color = '#f0ad4e' 406 | } else { 407 | //td.className = 'schedule-on'; 408 | td.style.background = '#f8d6a6' 409 | td.style.fontStyle = 'italic'; 410 | td.style.fontSize = 'smaller'; 411 | //td.style.textAlign = 'right'; 412 | //td.className = 'htRight'; 413 | } 414 | }; 415 | } 416 | } 417 | }) 418 | 419 | app.controller('ConfirmDeleteCtrl', function ConfirmDeleteCtrl($scope, $http, modalService) { 420 | $scope.deleteSchedule = function (schedName) { 421 | var modalOptions = { 422 | closeButtonText: 'Cancel', 423 | actionButtonText: 'Delete Schedule', 424 | headerText: 'Delete ' + schedName + '?', 425 | bodyText: 'Are you sure you want to delete this schedule?' 426 | }; 427 | 428 | modalService.showModal({}, modalOptions).then(function (result) { 429 | console.log("Delete confirmed!"); 430 | $http.post("/schedules", {'delete': schedName+'.json'}). 431 | success(function(data, status, headers, config) { 432 | console.log("scheduled POST successful") 433 | $scope.deleteRow(schedName); 434 | }). 435 | error(function(data, status, headers, config) { 436 | // log error 437 | console.log("scheduled POST error") 438 | }); 439 | }); 440 | } 441 | }); 442 | 443 | 444 | var modalInstanceCtrl = function ($scope, $modalInstance, conf) { 445 | 446 | $scope.conf = conf; 447 | //$scope.selected = { 448 | // item: $scope.items[0] 449 | //}; 450 | 451 | $scope.ok = function () { 452 | $modalInstance.close($scope.conf); 453 | }; 454 | 455 | $scope.cancel = function () { 456 | $modalInstance.dismiss('cancel'); 457 | }; 458 | }; 459 | -------------------------------------------------------------------------------- /config/components/editform.html: -------------------------------------------------------------------------------- 1 | 4 | 86 | -------------------------------------------------------------------------------- /config/components/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /config/components/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /config/components/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /config/components/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenW/Plugwise-2-py/a25d48dcb5c2cc625b2237bd23f1de2e3a08c960/config/components/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /config/components/font-awesome/version.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/components/hot/dist/jquery.handsontable.full.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Handsontable 0.11.0-beta3 3 | * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs 4 | * 5 | * Copyright 2012-2014 Marcin Warpechowski 6 | * Licensed under the MIT license. 7 | * http://handsontable.com/ 8 | * 9 | * Date: Fri Jul 25 2014 11:41:59 GMT+0200 (CEST) 10 | */ 11 | 12 | .handsontable { 13 | position: relative; 14 | } 15 | 16 | .handsontable .relative { 17 | position: relative; 18 | } 19 | 20 | .handsontable.htAutoColumnSize { 21 | visibility: hidden; 22 | left: 0; 23 | position: absolute; 24 | top: 0; 25 | } 26 | 27 | .handsontable table, 28 | .handsontable tbody, 29 | .handsontable thead, 30 | .handsontable td, 31 | .handsontable th, 32 | .handsontable div { 33 | box-sizing: content-box; 34 | -webkit-box-sizing: content-box; 35 | -moz-box-sizing: content-box; 36 | } 37 | 38 | .handsontable table.htCore { 39 | border-collapse: separate; 40 | /*it must be separate, otherwise there are offset miscalculations in WebKit: http://stackoverflow.com/questions/2655987/border-collapse-differences-in-ff-and-webkit*/ 41 | position: relative; 42 | /*this actually only changes appearance of user selection - does not make text unselectable 43 | -webkit-user-select: none; 44 | -khtml-user-select: none; 45 | -moz-user-select: none; 46 | -o-user-select: none; 47 | -ms-user-select: none; 48 | /*user-select: none; /*no browser supports unprefixed version*/ 49 | border-spacing: 0; 50 | margin: 0; 51 | border-width: 0; 52 | table-layout: fixed; 53 | width: 0; 54 | outline-width: 0; 55 | /* reset bootstrap table style. for more info see: https://github.com/handsontable/jquery-handsontable/issues/224 */ 56 | max-width: none; 57 | max-height: none; 58 | } 59 | 60 | .handsontable col { 61 | width: 50px; 62 | } 63 | 64 | .handsontable col.rowHeader { 65 | width: 50px; 66 | } 67 | 68 | .handsontable th, 69 | .handsontable td { 70 | border-right: 1px solid #CCC; 71 | border-bottom: 1px solid #CCC; 72 | height: 22px; 73 | empty-cells: show; 74 | line-height: 21px; 75 | padding: 0 4px 0 4px; 76 | /* top, bottom padding different than 0 is handled poorly by FF with HTML5 doctype */ 77 | background-color: #FFF; 78 | vertical-align: top; 79 | overflow: hidden; 80 | outline-width: 0; 81 | white-space: pre-line; 82 | /* preserve new line character in cell */ 83 | } 84 | 85 | .handsontable td.htInvalid { 86 | -webkit-transition: background 0.75s ease; 87 | transition: background 0.75s ease; 88 | background-color: #ff4c42; 89 | } 90 | 91 | .handsontable td.htNoWrap { 92 | white-space: nowrap; 93 | } 94 | 95 | .handsontable th:last-child { 96 | /*Foundation framework fix*/ 97 | border-right: 1px solid #CCC; 98 | border-bottom: 1px solid #CCC; 99 | } 100 | 101 | .handsontable tr:first-child th.htNoFrame, 102 | .handsontable th:first-child.htNoFrame, 103 | .handsontable th.htNoFrame { 104 | border-left-width: 0; 105 | background-color: white; 106 | border-color: #FFF; 107 | } 108 | 109 | .handsontable th:first-child, 110 | .handsontable td:first-child, 111 | .handsontable .htNoFrame + th, 112 | .handsontable .htNoFrame + td { 113 | border-left: 1px solid #CCC; 114 | } 115 | 116 | .handsontable tr:first-child th, 117 | .handsontable tr:first-child td { 118 | border-top: 1px solid #CCC; 119 | } 120 | 121 | .handsontable thead tr:last-child th { 122 | border-bottom-width: 0; 123 | } 124 | 125 | .handsontable thead tr.lastChild th { 126 | border-bottom-width: 0; 127 | } 128 | 129 | .handsontable th { 130 | background-color: #EEE; 131 | color: #222; 132 | text-align: center; 133 | font-weight: normal; 134 | white-space: nowrap; 135 | } 136 | 137 | .handsontable thead th { 138 | padding: 0; 139 | } 140 | 141 | .handsontable th.active { 142 | background-color: #CCC; 143 | } 144 | 145 | .handsontable thead th .relative { 146 | padding: 2px 4px; 147 | } 148 | 149 | /* plugins */ 150 | 151 | .handsontable .manualColumnMover { 152 | position: absolute; 153 | left: 0; 154 | top: 0; 155 | background-color: transparent; 156 | width: 5px; 157 | height: 25px; 158 | z-index: 999; 159 | cursor: move; 160 | } 161 | 162 | .handsontable .manualRowMover { 163 | position: absolute; 164 | left: -4px; 165 | top: 0; 166 | background-color: transparent; 167 | height: 5px; 168 | width: 50px; 169 | z-index: 999; 170 | cursor: move; 171 | } 172 | 173 | .handsontable .manualColumnMover:hover, 174 | .handsontable .manualColumnMover.active, 175 | .handsontable .manualRowMover:hover, 176 | .handsontable .manualRowMover.active{ 177 | background-color: #88F; 178 | } 179 | 180 | /* row + column resizer*/ 181 | 182 | .handsontable .manualColumnResizer { 183 | position: absolute; 184 | top: 0; 185 | cursor: col-resize; 186 | z-index: 100; 187 | } 188 | 189 | .handsontable .manualColumnResizerHandle { 190 | background-color: transparent; 191 | width: 5px; 192 | height: 25px; 193 | } 194 | 195 | .handsontable .manualRowResizer { 196 | position: absolute; 197 | left: 0; 198 | cursor: row-resize; 199 | z-index: 100; 200 | } 201 | 202 | .handsontable .manualRowResizerHandle { 203 | background-color: transparent; 204 | height: 5px; 205 | width: 50px; 206 | } 207 | 208 | .handsontable .manualColumnResizer:hover .manualColumnResizerHandle, 209 | .handsontable .manualColumnResizer.active .manualColumnResizerHandle, 210 | .handsontable .manualRowResizer:hover .manualRowResizerHandle, 211 | .handsontable .manualRowResizer.active .manualRowResizerHandle { 212 | background-color: #AAB; 213 | } 214 | 215 | .handsontable .manualColumnResizerLine { 216 | position: absolute; 217 | right: 0; 218 | top: 0; 219 | background-color: #AAB; 220 | display: none; 221 | width: 0; 222 | border-right: 1px dashed #777; 223 | } 224 | 225 | .handsontable .manualRowResizerLine { 226 | position: absolute; 227 | left: 0; 228 | bottom: 0; 229 | background-color: #AAB; 230 | display: none; 231 | width: 0; 232 | border-bottom: 1px dashed #777; 233 | } 234 | 235 | .handsontable .manualColumnResizer.active .manualColumnResizerLine, 236 | .handsontable .manualRowResizer.active .manualRowResizerLine { 237 | display: block; 238 | } 239 | 240 | .handsontable .columnSorting:hover { 241 | text-decoration: underline; 242 | cursor: pointer; 243 | } 244 | 245 | /* border line */ 246 | 247 | .handsontable .wtBorder { 248 | position: absolute; 249 | font-size: 0; 250 | } 251 | .handsontable .wtBorder.hidden{ 252 | display:none !important; 253 | } 254 | 255 | .handsontable td.area { 256 | background-color: #EEF4FF; 257 | } 258 | 259 | /* fill handle */ 260 | 261 | .handsontable .wtBorder.corner { 262 | font-size: 0; 263 | cursor: crosshair; 264 | } 265 | 266 | .handsontable .htBorder.htFillBorder { 267 | background: red; 268 | width: 1px; 269 | height: 1px; 270 | } 271 | 272 | .handsontableInput { 273 | border: 2px solid #5292F7; 274 | outline-width: 0; 275 | margin: 0; 276 | padding: 1px 4px 0 2px; 277 | font-family: Arial, Helvetica, sans-serif; 278 | /*repeat from .handsontable (inherit doesn't work with IE<8) */ 279 | line-height: 1.3em; 280 | /*repeat from .handsontable (inherit doesn't work with IE<8) */ 281 | font-size: inherit; 282 | -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 283 | box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 284 | resize: none; 285 | /*below are needed to overwrite stuff added by jQuery UI Bootstrap theme*/ 286 | display: inline-block; 287 | color: #000; 288 | border-radius: 0; 289 | background-color: #FFF; 290 | /*overwrite styles potentionally made by a framework*/ 291 | } 292 | 293 | .handsontableInputHolder { 294 | position: absolute; 295 | top: 0; 296 | left: 0; 297 | z-index: 100; 298 | } 299 | 300 | .htSelectEditor { 301 | -webkit-appearance: menulist-button !important; 302 | position: absolute; 303 | } 304 | 305 | /* 306 | TextRenderer readOnly cell 307 | */ 308 | 309 | .handsontable .htDimmed { 310 | color: #777; 311 | } 312 | 313 | /* 314 | TextRenderer horizontal alignment 315 | */ 316 | .handsontable .htLeft{ 317 | text-align: left; 318 | } 319 | .handsontable .htCenter{ 320 | text-align: center; 321 | } 322 | .handsontable .htRight{ 323 | text-align: right; 324 | } 325 | .handsontable .htJustify{ 326 | text-align: justify; 327 | } 328 | /* 329 | TextRenderer vertical alignment 330 | */ 331 | .handsontable .htTop{ 332 | vertical-align: top; 333 | } 334 | .handsontable .htMiddle{ 335 | vertical-align: middle; 336 | } 337 | .handsontable .htBottom{ 338 | vertical-align: bottom; 339 | } 340 | 341 | /* 342 | TextRenderer placeholder value 343 | */ 344 | 345 | .handsontable .htPlaceholder { 346 | color: #999; 347 | } 348 | 349 | /* 350 | AutocompleteRenderer down arrow 351 | */ 352 | 353 | .handsontable .htAutocompleteArrow { 354 | float: right; 355 | font-size: 10px; 356 | color: #EEE; 357 | cursor: default; 358 | width: 16px; 359 | text-align: center; 360 | } 361 | 362 | .handsontable td .htAutocompleteArrow:hover { 363 | color: #777; 364 | } 365 | 366 | /* 367 | CheckboxRenderer 368 | */ 369 | 370 | .handsontable .htCheckboxRendererInput.noValue { 371 | opacity: 0.5; 372 | } 373 | 374 | /* 375 | NumericRenderer 376 | */ 377 | 378 | .handsontable .htNumeric { 379 | text-align: right; 380 | } 381 | 382 | /* 383 | Comment For Cell 384 | */ 385 | .htCommentCell{ 386 | position: relative; 387 | } 388 | .htCommentCell:after{ 389 | content: ''; 390 | position: absolute; 391 | top: 0; 392 | right: 0; 393 | border-left: 6px solid transparent; 394 | /*border-right: 5px solid transparent;*/ 395 | 396 | border-top: 6px solid red; 397 | } 398 | 399 | /*context menu rules*/ 400 | 401 | ul.context-menu-list { 402 | color: black; 403 | } 404 | 405 | ul.context-menu-list li { 406 | margin-bottom: 0; 407 | /*Foundation framework fix*/ 408 | } 409 | 410 | /** 411 | * Handsontable in Handsontable 412 | */ 413 | 414 | .handsontable .handsontable .wtHider { 415 | padding: 0 0 5px 0; 416 | } 417 | 418 | .handsontable .handsontable table { 419 | -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 420 | box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 421 | } 422 | 423 | /** 424 | * Autocomplete Editor 425 | */ 426 | .handsontable .autocompleteEditor.handsontable { 427 | padding-right: 15px; 428 | } 429 | 430 | /** 431 | * Handsontable listbox theme 432 | */ 433 | 434 | .handsontable.listbox { 435 | margin: 0; 436 | } 437 | 438 | .handsontable.listbox .ht_master table { 439 | border: 1px solid #ccc; 440 | border-collapse: separate; 441 | background: white; 442 | } 443 | 444 | .handsontable.listbox th, 445 | .handsontable.listbox tr:first-child th, 446 | .handsontable.listbox tr:last-child th, 447 | .handsontable.listbox tr:first-child td, 448 | .handsontable.listbox td { 449 | border-width: 0; 450 | } 451 | 452 | .handsontable.listbox th, 453 | .handsontable.listbox td { 454 | white-space: nowrap; 455 | text-overflow: ellipsis; 456 | } 457 | 458 | .handsontable.listbox td.htDimmed { 459 | cursor: default; 460 | color: inherit; 461 | font-style: inherit; 462 | } 463 | 464 | .handsontable.listbox .wtBorder { 465 | visibility: hidden; 466 | } 467 | 468 | .handsontable.listbox tr td.current, 469 | .handsontable.listbox tr:hover td { 470 | background: #eee; 471 | } 472 | 473 | .htContextMenu { 474 | display: none; 475 | position: absolute; 476 | } 477 | 478 | .htContextMenu .ht_clone_top, 479 | .htContextMenu .ht_clone_left, 480 | .htContextMenu .ht_clone_corner { 481 | display: none; 482 | } 483 | 484 | .htContextMenu table.htCore { 485 | outline: 1px solid #bbb; 486 | } 487 | 488 | .htContextMenu .wtBorder { 489 | visibility: hidden; 490 | } 491 | 492 | .htContextMenu table tbody tr td { 493 | background: white; 494 | border-width: 0; 495 | padding: 4px 6px 0px 6px; 496 | cursor: pointer; 497 | overflow: hidden; 498 | white-space: nowrap; 499 | text-overflow: ellipsis; 500 | } 501 | 502 | .htContextMenu table tbody tr td:first-child { 503 | border: 0; 504 | } 505 | 506 | .htContextMenu table tbody tr td.htDimmed{ 507 | font-style: normal; 508 | color: #323232; 509 | } 510 | 511 | .htContextMenu table tbody tr td.current{ 512 | background: rgb(233, 233, 233); 513 | } 514 | 515 | .htContextMenu table tbody tr td.htSeparator { 516 | border-top: 1px solid #bbb; 517 | height: 0; 518 | padding: 0; 519 | } 520 | 521 | .htContextMenu table tbody tr td.htDisabled { 522 | color: #999; 523 | } 524 | 525 | .htContextMenu table tbody tr td.htDisabled:hover { 526 | background: white; 527 | color: #999; 528 | cursor: default; 529 | } 530 | 531 | .htContextMenu button{ 532 | color: #fff; 533 | font-size: 12px; 534 | line-height: 12px; 535 | height: 20px; 536 | padding: 0 5px; 537 | margin: 0 1px 0 0; 538 | margin-bottom: 5px; 539 | } 540 | .htContextMenu button:first-child{ 541 | border-top-left-radius: 5px; 542 | border-bottom-left-radius: 5px; 543 | } 544 | .htContextMenu button:last-child{ 545 | border-top-right-radius: 5px; 546 | border-bottom-right-radius: 5px; 547 | } 548 | .handsontable td.htSearchResult { 549 | background: #fcedd9; 550 | color: #583707; 551 | } 552 | 553 | /* 554 | Cell borders 555 | */ 556 | .htBordered{ 557 | /*box-sizing: border-box !important;*/ 558 | border-width: 1px; 559 | } 560 | .htBordered.htTopBorderSolid{ 561 | border-top-style: solid; 562 | border-top-color: #000; 563 | } 564 | .htBordered.htRightBorderSolid{ 565 | border-right-style: solid; 566 | border-right-color: #000; 567 | } 568 | .htBordered.htBottomBorderSolid{ 569 | border-bottom-style: solid; 570 | border-bottom-color: #000; 571 | } 572 | .htBordered.htLeftBorderSolid{ 573 | border-left-style: solid; 574 | border-left-color: #000; 575 | } 576 | 577 | /*WalkontableDebugOverlay*/ 578 | 579 | .wtDebugHidden { 580 | display: none; 581 | } 582 | 583 | .wtDebugVisible { 584 | display: block; 585 | -webkit-animation-duration: 0.5s; 586 | -webkit-animation-name: wtFadeInFromNone; 587 | animation-duration: 0.5s; 588 | animation-name: wtFadeInFromNone; 589 | } 590 | 591 | @keyframes wtFadeInFromNone { 592 | 0% { 593 | display: none; 594 | opacity: 0; 595 | } 596 | 597 | 1% { 598 | display: block; 599 | opacity: 0; 600 | } 601 | 602 | 100% { 603 | display: block; 604 | opacity: 1; 605 | } 606 | } 607 | 608 | @-webkit-keyframes wtFadeInFromNone { 609 | 0% { 610 | display: none; 611 | opacity: 0; 612 | } 613 | 614 | 1% { 615 | display: block; 616 | opacity: 0; 617 | } 618 | 619 | 100% { 620 | display: block; 621 | opacity: 1; 622 | } 623 | } -------------------------------------------------------------------------------- /config/components/modal.html: -------------------------------------------------------------------------------- 1 | 4 | 7 | 13 | -------------------------------------------------------------------------------- /config/components/modal.js: -------------------------------------------------------------------------------- 1 | angular.module('schedules').service('modalService', ['$modal', 2 | function ($modal) { 3 | 4 | var modalDefaults = { 5 | backdrop: true, 6 | keyboard: true, 7 | modalFade: true, 8 | templateUrl: '/components/modal.html' 9 | }; 10 | 11 | var modalOptions = { 12 | closeButtonText: 'Close', 13 | actionButtonText: 'OK', 14 | headerText: 'Proceed?', 15 | bodyText: 'Perform this action?' 16 | }; 17 | 18 | this.showModal = function (customModalDefaults, customModalOptions) { 19 | if (!customModalDefaults) customModalDefaults = {}; 20 | customModalDefaults.backdrop = 'static'; 21 | return this.show(customModalDefaults, customModalOptions); 22 | }; 23 | 24 | this.show = function (customModalDefaults, customModalOptions) { 25 | //Create temp objects to work with since we're in a singleton service 26 | var tempModalDefaults = {}; 27 | var tempModalOptions = {}; 28 | 29 | //Map angular-ui modal custom defaults to modal defaults defined in service 30 | angular.extend(tempModalDefaults, modalDefaults, customModalDefaults); 31 | 32 | //Map modal.html $scope custom properties to defaults defined in service 33 | angular.extend(tempModalOptions, modalOptions, customModalOptions); 34 | 35 | if (!tempModalDefaults.controller) { 36 | tempModalDefaults.controller = function ($scope, $modalInstance) { 37 | $scope.modalOptions = tempModalOptions; 38 | $scope.modalOptions.ok = function (result) { 39 | $modalInstance.close(result); 40 | }; 41 | $scope.modalOptions.close = function (result) { 42 | $modalInstance.dismiss('cancel'); 43 | }; 44 | } 45 | } 46 | 47 | return $modal.open(tempModalDefaults).result; 48 | }; 49 | 50 | }]); 51 | -------------------------------------------------------------------------------- /config/components/pw2py.css: -------------------------------------------------------------------------------- 1 | body{ 2 | padding:20px; 3 | } 4 | .search{ 5 | margin-left:10px; 6 | } 7 | 8 | /*Cardview*/ 9 | .cardContainer { 10 | /*width:85%;*/ 11 | } 12 | 13 | .card { 14 | background-color:#fff; 15 | border: 1px solid #d4d4d4; 16 | /*height:300px;*/ 17 | /*min-width: 280px;*/ 18 | /*width: 280px;*/ 19 | margin-bottom: 20px; 20 | /*position: relative;*/ 21 | border-radius: 10px 10px 10px 10px; 22 | font-size:12pt; 23 | } 24 | 25 | .cardHeader { 26 | background-color: #428bca; 27 | /*background-color: #5bc0de;*/ 28 | /*background-color:#027FF4;*/ 29 | font-size:16pt; 30 | color:white; 31 | padding:5px; 32 | /*width:100%;*/ 33 | border-radius: 10px 10px 0px 0px; 34 | 35 | } 36 | 37 | .cardClose { 38 | color: white; 39 | font-weight:bold; 40 | margin-right:5px; 41 | } 42 | 43 | 44 | .cardBody { 45 | padding-left: 5px; 46 | } 47 | 48 | .cardBodyLeft { 49 | /*margin-top: 20px;*/ 50 | } 51 | 52 | .cardBodyRight { 53 | /*margin-left: 20px;*/ 54 | margin-top: 2px; 55 | } 56 | 57 | .cardBodyContent { 58 | /*width: 100px;*/ 59 | /*margin-left: 20px;*/ 60 | /*margin-top: 20px;*/ 61 | 62 | } 63 | 64 | .cardImage { 65 | /*height:50px;width:50px;margin-top:20px;margin-left:20px;*/ 66 | /*min-width:100%;*/ 67 | min-height:75%; 68 | /*height: 100%;*/ 69 | /*width: 100%;*/ 70 | } 71 | 72 | .cardIcon { 73 | padding-left: 0px; 74 | padding-right: 0px; 75 | } 76 | 77 | .cardReading { 78 | font-size: 22pt; 79 | font-weight:bold; 80 | white-space: nowrap; 81 | } 82 | .cardInfo { 83 | font-size: 12pt; 84 | color: #bbb; 85 | white-space: nowrap; 86 | float: right; 87 | margin-right: -10px; 88 | } 89 | 90 | .cardBig { 91 | font-size: 30pt; 92 | } 93 | 94 | .cardBigBig { 95 | font-size: 40pt; 96 | } 97 | 98 | .cardOn { 99 | color: #E6B800; 100 | /*color: #FFCC00;*/ 101 | } 102 | 103 | .cardOff { 104 | color: #bbb; 105 | } 106 | 107 | .production { 108 | color: green; 109 | } 110 | .vsize50 { 111 | height: 50px; 112 | } 113 | .vsize100 { 114 | height: 100px; 115 | } 116 | .vflex1 { 117 | display: -webkit-flex; 118 | display: flex; 119 | -webkit-flex-direction: row /* works with row or column */ 120 | flex-direction: row; 121 | -webkit-align-items: center; 122 | align-items: center; 123 | -webkit-justify-content: center; 124 | justify-content: center; 125 | } 126 | 127 | .vcenter { 128 | display: -webkit-box; 129 | -webkit-box-orient: horizontal; 130 | -webkit-box-pack: center; 131 | -webkit-box-align: center; 132 | 133 | display: -moz-box; 134 | -moz-box-orient: horizontal; 135 | -moz-box-pack: center; 136 | -moz-box-align: center; 137 | 138 | display: box; 139 | box-orient: horizontal; 140 | box-pack: center; 141 | box-align: center; 142 | } 143 | 144 | .vcenter1{ 145 | /*margin: 0;*/ 146 | position: absolute; 147 | top: 50%; 148 | transform: translate(0, -50%) 149 | } 150 | 151 | .vcenter2 { 152 | display: inline-block; 153 | vertical-align: middle; 154 | float: none; 155 | } 156 | 157 | .vfull { 158 | height: 100%; 159 | } 160 | 161 | /*fix tooltip width bootstrap*/ 162 | .tooltip-inner { 163 | max-width: 350px; 164 | /* If max-width does not work, try using width instead */ 165 | width: 250px; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /config/components/pw2py.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('pw2pyapp', ['ui.bootstrap']); 2 | 3 | app.controller("pw2pyCtrl", function pw2pyCtrl($scope, $http, WS){ 4 | $scope.conf = {static: []}; 5 | $scope.config = {dynamic: []}; 6 | $scope.circles = []; 7 | $scope.obj = {state: false}; 8 | $scope.websockets = true; 9 | 10 | 11 | $scope.loadConfig = function () { 12 | console.log("loadConfig"); 13 | $http.get("/pw-conf.json"). 14 | success(function(data, status, headers, config) { 15 | $scope.conf = data; 16 | $http.get("/pw-control.json"). 17 | success(function(data, status, headers, config) { 18 | $scope.config = data; 19 | //Merge config 20 | for (var i=0; i<$scope.conf.static.length; i++) { 21 | var statconf = $scope.conf.static[i]; 22 | var circle = {}; 23 | for (var key in statconf) { circle[key] = statconf[key]; }; 24 | var dynconf = getByMac($scope.config.dynamic, circle.mac); 25 | if (dynconf != null) { 26 | for (var key in dynconf) { circle[key] = dynconf[key]; }; 27 | } 28 | circle['alwayson'] = (circle.always_on == "True") ? true : false 29 | if (circle['alwayson']) { 30 | circle.switch_state = 'on'; 31 | circle.schedule_state = 'off'; 32 | } 33 | circle['power'] = "-"; 34 | circle['relayon'] = circle.switch_state 35 | 36 | circle.toolTip = "interval: " + circle.loginterval + " min.
" 37 | circle.toolTip += "monitor (10s): " + circle.monitor + "
" 38 | circle.toolTip += "save log (" + circle.loginterval + "m): " + circle.savelog + "
" 39 | circle.toolTip += "mac: " + circle.mac 40 | 41 | circle.icon = "fa-lightbulb-o" 42 | if (circle.category == "PV") { 43 | circle.icon = "fa-bolt" 44 | } else if (circle.category == "divers") { 45 | circle.icon = "fa-plug" 46 | } 47 | 48 | //TESTCODE 49 | //circle.alwayson = !(circle.name == 'circle+'); 50 | 51 | 52 | $scope.circles.push(circle); 53 | console.log(dynconf); 54 | console.log(statconf); 55 | console.log(circle); 56 | 57 | //WS.connect(); 58 | } 59 | }). 60 | error(function(data, status, headers, config) { 61 | // log error 62 | }); 63 | }). 64 | error(function(data, status, headers, config) { 65 | // log error 66 | }); 67 | }; 68 | 69 | function getByMac(arr, mac) { 70 | for (var i=0; i', 206 | replace : true, 207 | scope: { 208 | switch: '='//, 209 | //state: '=' 210 | }, 211 | 212 | link : function(scope, element, attrs/*, ngModel*/){ 213 | // // Listen for the button click event to enable binding 214 | // element.bind('click', function() { 215 | // scope.$apply(toggle); 216 | // }); 217 | 218 | // // Toggle the model value 219 | // function toggle() { 220 | // var val = ngModel.$viewValue; 221 | // ngModel.$setViewValue(!val); 222 | // render(); 223 | // } 224 | scope.state = true; 225 | scope.turnSwitchOn = function (id) { 226 | console.log("switch on") 227 | console.log(id) 228 | scope.state = true; 229 | }; 230 | scope.turnSwitchOff = function (id) { 231 | console.log("switch off") 232 | console.log(id) 233 | scope.state = false; 234 | }; 235 | } 236 | }; 237 | }); 238 | 239 | app.factory('WS', function() { 240 | var service = {}; 241 | 242 | service.connect = function() { 243 | if(service.ws) { return; } 244 | 245 | //var host = window.location.href.split("/")[2]; 246 | var host = window.location.host; 247 | var ws; 248 | if (window.location.protocol == 'https:') { 249 | //ws = new WebSocket("wss://"+host+"/socket.ws"); 250 | ws = new ReconnectingWebSocket("wss://"+host+"/socket.ws"); 251 | } else { 252 | //ws = new WebSocket("ws://"+host+"/socket.ws"); 253 | ws = new ReconnectingWebSocket("ws://"+host+"/socket.ws"); 254 | } 255 | 256 | ws.onopen = function() { 257 | service.callback('{"result": "Succeeded to open a connection"}'); 258 | }; 259 | 260 | ws.onerror = function() { 261 | service.callback('{"result": "Failed to open a connection"}'); 262 | } 263 | 264 | ws.onmessage = function(message) { 265 | service.callback(message.data); 266 | }; 267 | 268 | service.ws = ws; 269 | } 270 | 271 | service.disconnect = function(message) { 272 | service.ws.close(); 273 | service.ws = null 274 | } 275 | 276 | service.send = function(message) { 277 | service.ws.send(message); 278 | } 279 | 280 | service.subscribe = function(callback) { 281 | service.callback = callback; 282 | } 283 | 284 | return service; 285 | }); 286 | 287 | // app.controller("WSCtrl", function WSCtrl($scope, WS) { 288 | // //$scope.messages = []; 289 | // $scope.lastmessage = {}; 290 | 291 | // WS.subscribe(function(message) { 292 | // //$scope.messages.push(message); 293 | // $scope.lastmessage = JSON.parse(message); 294 | // $scope.$apply(); 295 | // }); 296 | 297 | // $scope.connect = function() { 298 | // WS.connect(); 299 | // } 300 | 301 | // $scope.send = function() { 302 | // WS.send($scope.text); 303 | // $scope.text = ""; 304 | // } 305 | // }) 306 | -------------------------------------------------------------------------------- /config/components/reconnecting-websocket/reconnecting-websocket.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2012, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * This behaves like a WebSocket in every way, except if it fails to connect, 25 | * or it gets disconnected, it will repeatedly poll until it successfully connects 26 | * again. 27 | * 28 | * It is API compatible, so when you have: 29 | * ws = new WebSocket('ws://....'); 30 | * you can replace with: 31 | * ws = new ReconnectingWebSocket('ws://....'); 32 | * 33 | * The event stream will typically look like: 34 | * onconnecting 35 | * onopen 36 | * onmessage 37 | * onmessage 38 | * onclose // lost connection 39 | * onconnecting 40 | * onopen // sometime later... 41 | * onmessage 42 | * onmessage 43 | * etc... 44 | * 45 | * It is API compatible with the standard WebSocket API, apart from the following members: 46 | * 47 | * - `bufferedAmount` 48 | * - `extensions` 49 | * - `binaryType` 50 | * 51 | * Latest version: https://github.com/joewalnes/reconnecting-websocket/ 52 | * - Joe Walnes 53 | * 54 | * Syntax 55 | * ====== 56 | * var socket = new ReconnectingWebSocket(url, protocols, options); 57 | * 58 | * Parameters 59 | * ========== 60 | * url - The url you are connecting to. 61 | * protocols - Optional string or array of protocols. 62 | * options - See below 63 | * 64 | * Options 65 | * ======= 66 | * Options can either be passed upon instantiation or set after instantiation: 67 | * 68 | * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 }); 69 | * 70 | * or 71 | * 72 | * var socket = new ReconnectingWebSocket(url); 73 | * socket.debug = true; 74 | * socket.reconnectInterval = 4000; 75 | * 76 | * debug 77 | * - Whether this instance should log debug messages. Accepts true or false. Default: false. 78 | * 79 | * automaticOpen 80 | * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close(). 81 | * 82 | * reconnectInterval 83 | * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000. 84 | * 85 | * maxReconnectInterval 86 | * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000. 87 | * 88 | * reconnectDecay 89 | * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5. 90 | * 91 | * timeoutInterval 92 | * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000. 93 | * 94 | */ 95 | (function (global, factory) { 96 | if (typeof define === 'function' && define.amd) { 97 | define([], factory); 98 | } else if (typeof module !== 'undefined' && module.exports){ 99 | module.exports = factory(); 100 | } else { 101 | global.ReconnectingWebSocket = factory(); 102 | } 103 | })(this, function () { 104 | 105 | if (!('WebSocket' in window)) { 106 | return; 107 | } 108 | 109 | function ReconnectingWebSocket(url, protocols, options) { 110 | 111 | // Default settings 112 | var settings = { 113 | 114 | /** Whether this instance should log debug messages. */ 115 | debug: false, 116 | 117 | /** Whether or not the websocket should attempt to connect immediately upon instantiation. */ 118 | automaticOpen: true, 119 | 120 | /** The number of milliseconds to delay before attempting to reconnect. */ 121 | reconnectInterval: 1000, 122 | /** The maximum number of milliseconds to delay a reconnection attempt. */ 123 | maxReconnectInterval: 30000, 124 | /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */ 125 | reconnectDecay: 1.5, 126 | 127 | /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */ 128 | timeoutInterval: 2000, 129 | 130 | /** The maximum number of reconnection attempts to make. Unlimited if null. */ 131 | maxReconnectAttempts: null 132 | } 133 | if (!options) { options = {}; } 134 | 135 | // Overwrite and define settings with options if they exist. 136 | for (var key in settings) { 137 | if (typeof options[key] !== 'undefined') { 138 | this[key] = options[key]; 139 | } else { 140 | this[key] = settings[key]; 141 | } 142 | } 143 | 144 | // These should be treated as read-only properties 145 | 146 | /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */ 147 | this.url = url; 148 | 149 | /** The number of attempted reconnects since starting, or the last successful connection. Read only. */ 150 | this.reconnectAttempts = 0; 151 | 152 | /** 153 | * The current state of the connection. 154 | * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED 155 | * Read only. 156 | */ 157 | this.readyState = WebSocket.CONNECTING; 158 | 159 | /** 160 | * A string indicating the name of the sub-protocol the server selected; this will be one of 161 | * the strings specified in the protocols parameter when creating the WebSocket object. 162 | * Read only. 163 | */ 164 | this.protocol = null; 165 | 166 | // Private state variables 167 | 168 | var self = this; 169 | var ws; 170 | var forcedClose = false; 171 | var timedOut = false; 172 | var eventTarget = document.createElement('div'); 173 | 174 | // Wire up "on*" properties as event handlers 175 | 176 | eventTarget.addEventListener('open', function(event) { self.onopen(event); }); 177 | eventTarget.addEventListener('close', function(event) { self.onclose(event); }); 178 | eventTarget.addEventListener('connecting', function(event) { self.onconnecting(event); }); 179 | eventTarget.addEventListener('message', function(event) { self.onmessage(event); }); 180 | eventTarget.addEventListener('error', function(event) { self.onerror(event); }); 181 | 182 | // Expose the API required by EventTarget 183 | 184 | this.addEventListener = eventTarget.addEventListener.bind(eventTarget); 185 | this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); 186 | this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); 187 | 188 | /** 189 | * This function generates an event that is compatible with standard 190 | * compliant browsers and IE9 - IE11 191 | * 192 | * This will prevent the error: 193 | * Object doesn't support this action 194 | * 195 | * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563 196 | * @param s String The name that the event should use 197 | * @param args Object an optional object that the event will use 198 | */ 199 | function generateEvent(s, args) { 200 | var evt = document.createEvent("CustomEvent"); 201 | evt.initCustomEvent(s, false, false, args); 202 | return evt; 203 | }; 204 | 205 | this.open = function (reconnectAttempt) { 206 | ws = new WebSocket(self.url, protocols || []); 207 | 208 | if (reconnectAttempt) { 209 | if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) { 210 | return; 211 | } 212 | } else { 213 | eventTarget.dispatchEvent(generateEvent('connecting')); 214 | this.reconnectAttempts = 0; 215 | } 216 | 217 | if (self.debug || ReconnectingWebSocket.debugAll) { 218 | console.debug('ReconnectingWebSocket', 'attempt-connect', self.url); 219 | } 220 | 221 | var localWs = ws; 222 | var timeout = setTimeout(function() { 223 | if (self.debug || ReconnectingWebSocket.debugAll) { 224 | console.debug('ReconnectingWebSocket', 'connection-timeout', self.url); 225 | } 226 | timedOut = true; 227 | localWs.close(); 228 | timedOut = false; 229 | }, self.timeoutInterval); 230 | 231 | ws.onopen = function(event) { 232 | clearTimeout(timeout); 233 | if (self.debug || ReconnectingWebSocket.debugAll) { 234 | console.debug('ReconnectingWebSocket', 'onopen', self.url); 235 | } 236 | self.protocol = ws.protocol; 237 | self.readyState = WebSocket.OPEN; 238 | self.reconnectAttempts = 0; 239 | var e = generateEvent('open'); 240 | e.isReconnect = reconnectAttempt; 241 | reconnectAttempt = false; 242 | eventTarget.dispatchEvent(e); 243 | }; 244 | 245 | ws.onclose = function(event) { 246 | clearTimeout(timeout); 247 | ws = null; 248 | if (forcedClose) { 249 | self.readyState = WebSocket.CLOSED; 250 | eventTarget.dispatchEvent(generateEvent('close')); 251 | } else { 252 | self.readyState = WebSocket.CONNECTING; 253 | var e = generateEvent('connecting'); 254 | e.code = event.code; 255 | e.reason = event.reason; 256 | e.wasClean = event.wasClean; 257 | eventTarget.dispatchEvent(e); 258 | if (!reconnectAttempt && !timedOut) { 259 | if (self.debug || ReconnectingWebSocket.debugAll) { 260 | console.debug('ReconnectingWebSocket', 'onclose', self.url); 261 | } 262 | eventTarget.dispatchEvent(generateEvent('close')); 263 | } 264 | 265 | var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts); 266 | setTimeout(function() { 267 | self.reconnectAttempts++; 268 | self.open(true); 269 | }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout); 270 | } 271 | }; 272 | ws.onmessage = function(event) { 273 | if (self.debug || ReconnectingWebSocket.debugAll) { 274 | console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data); 275 | } 276 | var e = generateEvent('message'); 277 | e.data = event.data; 278 | eventTarget.dispatchEvent(e); 279 | }; 280 | ws.onerror = function(event) { 281 | if (self.debug || ReconnectingWebSocket.debugAll) { 282 | console.debug('ReconnectingWebSocket', 'onerror', self.url, event); 283 | } 284 | eventTarget.dispatchEvent(generateEvent('error')); 285 | }; 286 | } 287 | 288 | // Whether or not to create a websocket upon instantiation 289 | if (this.automaticOpen == true) { 290 | this.open(false); 291 | } 292 | 293 | /** 294 | * Transmits data to the server over the WebSocket connection. 295 | * 296 | * @param data a text string, ArrayBuffer or Blob to send to the server. 297 | */ 298 | this.send = function(data) { 299 | if (ws) { 300 | if (self.debug || ReconnectingWebSocket.debugAll) { 301 | console.debug('ReconnectingWebSocket', 'send', self.url, data); 302 | } 303 | return ws.send(data); 304 | } else { 305 | throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; 306 | } 307 | }; 308 | 309 | /** 310 | * Closes the WebSocket connection or connection attempt, if any. 311 | * If the connection is already CLOSED, this method does nothing. 312 | */ 313 | this.close = function(code, reason) { 314 | // Default CLOSE_NORMAL code 315 | if (typeof code == 'undefined') { 316 | code = 1000; 317 | } 318 | forcedClose = true; 319 | if (ws) { 320 | ws.close(code, reason); 321 | } 322 | }; 323 | 324 | /** 325 | * Additional public API method to refresh the connection if still open (close, re-open). 326 | * For example, if the app suspects bad data / missed heart beats, it can try to refresh. 327 | */ 328 | this.refresh = function() { 329 | if (ws) { 330 | ws.close(); 331 | } 332 | }; 333 | } 334 | 335 | /** 336 | * An event listener to be called when the WebSocket connection's readyState changes to OPEN; 337 | * this indicates that the connection is ready to send and receive data. 338 | */ 339 | ReconnectingWebSocket.prototype.onopen = function(event) {}; 340 | /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */ 341 | ReconnectingWebSocket.prototype.onclose = function(event) {}; 342 | /** An event listener to be called when a connection begins being attempted. */ 343 | ReconnectingWebSocket.prototype.onconnecting = function(event) {}; 344 | /** An event listener to be called when a message is received from the server. */ 345 | ReconnectingWebSocket.prototype.onmessage = function(event) {}; 346 | /** An event listener to be called when an error occurs. */ 347 | ReconnectingWebSocket.prototype.onerror = function(event) {}; 348 | 349 | /** 350 | * Whether all instances of ReconnectingWebSocket should log debug messages. 351 | * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. 352 | */ 353 | ReconnectingWebSocket.debugAll = false; 354 | 355 | ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING; 356 | ReconnectingWebSocket.OPEN = WebSocket.OPEN; 357 | ReconnectingWebSocket.CLOSING = WebSocket.CLOSING; 358 | ReconnectingWebSocket.CLOSED = WebSocket.CLOSED; 359 | 360 | return ReconnectingWebSocket; 361 | }); -------------------------------------------------------------------------------- /config/components/reconnecting-websocket/reconnecting-websocket.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); -------------------------------------------------------------------------------- /config/components/switch.css: -------------------------------------------------------------------------------- 1 | /* 2 | *,:before,:after{ 3 | box-sizing:border-box; 4 | margin:0; 5 | padding:0; 6 | /*transition * / 7 | -webkit-transition:.25s ease-in-out; 8 | -moz-transition:.25s ease-in-out; 9 | -o-transition:.25s ease-in-out; 10 | transition:.25s ease-in-out; 11 | outline:none; 12 | font-family:Helvetica Neue,helvetica,arial,verdana,sans-serif; 13 | } 14 | */ 15 | #toggles{ 16 | width:60px; 17 | margin:0px 40px 0px 40px; 18 | text-align:center; 19 | } 20 | .ios-toggle,.ios-toggle:active{ 21 | position:absolute; 22 | top:-5000px; 23 | height:0; 24 | width:0; 25 | opacity:0; 26 | border:none; 27 | outline:none; 28 | } 29 | .checkbox-label{ 30 | display:block; 31 | position:relative; 32 | padding:10px; 33 | margin-bottom:5px; 34 | font-size:16px; 35 | line-height:16px; 36 | width:100%; 37 | height:36px; 38 | /*border-radius*/ 39 | -webkit-border-radius:18px; 40 | -moz-border-radius:18px; 41 | border-radius:18px; 42 | background:#cccccc; 43 | cursor:pointer; 44 | } 45 | .checkbox-label:before{ 46 | content:''; 47 | display:block; 48 | position:absolute; 49 | z-index:1; 50 | line-height:34px; 51 | text-indent:40px; 52 | height:36px; 53 | width:36px; 54 | /*border-radius*/ 55 | -webkit-border-radius:100%; 56 | -moz-border-radius:100%; 57 | border-radius:100%; 58 | top:0px; 59 | left:0px; 60 | right:auto; 61 | background:white; 62 | /*box-shadow*/ 63 | -webkit-box-shadow:0 3px 3px rgba(0,0,0,.2),0 0 0 2px #bbbbbb; 64 | -moz-box-shadow:0 3px 3px rgba(0,0,0,.2),0 0 0 2px #bbbbbb; 65 | box-shadow:0 3px 3px rgba(0,0,0,.2),0 0 0 2px #bbbbbb; 66 | } 67 | .checkbox-label:after{ 68 | content:attr(data-off); 69 | display:block; 70 | position:absolute; 71 | z-index:0; 72 | top:0; 73 | left:-36px; 74 | padding:10px; 75 | height:100%; 76 | width:36px; 77 | text-align:right; 78 | color:#bbbbbb; 79 | white-space:nowrap; 80 | } 81 | .ios-toggle:checked + .checkbox-label{ 82 | /*box-shadow*/ 83 | -webkit-box-shadow:inset 0 0 0 20px rgba(19,191,17,1),0 0 0 2px rgba(19,191,17,1); 84 | -moz-box-shadow:inset 0 0 0 20px rgba(19,191,17,1),0 0 0 2px rgba(19,191,17,1); 85 | box-shadow:inset 0 0 0 20px rgba(19,191,17,1),0 0 0 2px rgba(19,191,17,1); 86 | } 87 | .ios-toggle:checked + .checkbox-label:before{ 88 | left:calc(100% - 36px); 89 | /*box-shadow*/ 90 | -webkit-box-shadow:0 0 0 2px transparent,0 3px 3px rgba(0,0,0,.3); 91 | -moz-box-shadow:0 0 0 2px transparent,0 3px 3px rgba(0,0,0,.3); 92 | box-shadow:0 0 0 2px transparent,0 3px 3px rgba(0,0,0,.3); 93 | } 94 | .ios-toggle:checked + .checkbox-label:after{ 95 | content:attr(data-on); 96 | left:60px; 97 | width:36px; 98 | } 99 | .ios-toggle-warning + .checkbox-label{ 100 | /*box-shadow*/ 101 | -webkit-box-shadow:inset 0 0 0 0px #ffcc00,0 0 0 2px #bbbbbb; 102 | -moz-box-shadow:inset 0 0 0 0px #ffcc00,0 0 0 2px #bbbbbb; 103 | box-shadow:inset 0 0 0 0px #ffcc00,0 0 0 2px #bbbbbb; 104 | } 105 | .ios-toggle-warning:checked + .checkbox-label{ 106 | /*box-shadow*/ 107 | -webkit-box-shadow:inset 0 0 0 20px #ffcc00,0 0 0 2px #eea236; 108 | -moz-box-shadow:inset 0 0 0 20px #ffcc00,0 0 0 2px #eea236; 109 | box-shadow:inset 0 0 0 20px #ffcc00,0 0 0 2px #eea236; 110 | } 111 | .ios-toggle-warning:checked + .checkbox-label:after{ 112 | color:#eea236; 113 | } 114 | .ios-toggle-disabled + .checkbox-label{ 115 | background:#eeeeee; 116 | color:#bbbbbb; 117 | /*box-shadow*/ 118 | -webkit-box-shadow:inset 0 0 0 0px #eeeeee,0 0 0 2px #dddddd; 119 | -moz-box-shadow:inset 0 0 0 0px #eeeeee,0 0 0 2px #dddddd; 120 | box-shadow:inset 0 0 0 0px #eeeeee,0 0 0 2px #dddddd; 121 | } 122 | .ios-toggle-disabled + .checkbox-label:before{ 123 | /*box-shadow*/ 124 | -webkit-box-shadow:inset 0 0 0 0px #eeeeee,0 0 0 2px #dddddd; 125 | -moz-box-shadow:inset 0 0 0 0px #eeeeee,0 0 0 2px #dddddd; 126 | box-shadow:inset 0 0 0 0px #eeeeee,0 0 0 2px #dddddd; 127 | } 128 | .ios-toggle-disabled:checked + .checkbox-label{ 129 | /*box-shadow*/ 130 | -webkit-box-shadow:inset 0 0 0 20px #FFE680,0 0 0 2px #dddddd; 131 | -moz-box-shadow:inset 0 0 0 20px #FFE680,0 0 0 2px #dddddd; 132 | box-shadow:inset 0 0 0 20px #FFE680,0 0 0 2px #dddddd; 133 | } 134 | .ios-toggle-disabled:checked + .checkbox-label:after{ 135 | color:#bbbbbb; 136 | } 137 | 138 | /* GREEN CHECKBOX */ 139 | 140 | #checkbox1 + .checkbox-label{ 141 | /*box-shadow*/ 142 | -webkit-box-shadow:inset 0 0 0 0px rgba(19,191,17,1),0 0 0 2px #dddddd; 143 | -moz-box-shadow:inset 0 0 0 0px rgba(19,191,17,1),0 0 0 2px #dddddd; 144 | box-shadow:inset 0 0 0 0px rgba(19,191,17,1),0 0 0 2px #dddddd; 145 | } 146 | #checkbox1:checked + .checkbox-label{ 147 | /*box-shadow*/ 148 | -webkit-box-shadow:inset 0 0 0 18px rgba(19,191,17,1),0 0 0 2px rgba(19,191,17,1); 149 | -moz-box-shadow:inset 0 0 0 18px rgba(19,191,17,1),0 0 0 2px rgba(19,191,17,1); 150 | box-shadow:inset 0 0 0 18px rgba(19,191,17,1),0 0 0 2px rgba(19,191,17,1); 151 | } 152 | #checkbox1:checked + .checkbox-label:after{ 153 | color:rgba(19,191,17,1); 154 | } 155 | /* RED CHECKBOX */ 156 | 157 | #checkbox2 + .checkbox-label{ 158 | /*box-shadow*/ 159 | -webkit-box-shadow:inset 0 0 0 0px #f35f42,0 0 0 2px #dddddd; 160 | -moz-box-shadow:inset 0 0 0 0px #f35f42,0 0 0 2px #dddddd; 161 | box-shadow:inset 0 0 0 0px #f35f42,0 0 0 2px #dddddd; 162 | } 163 | #checkbox2:checked + .checkbox-label{ 164 | /*box-shadow*/ 165 | -webkit-box-shadow:inset 0 0 0 20px #f35f42,0 0 0 2px #f35f42; 166 | -moz-box-shadow:inset 0 0 0 20px #f35f42,0 0 0 2px #f35f42; 167 | box-shadow:inset 0 0 0 20px #f35f42,0 0 0 2px #f35f42; 168 | } 169 | #checkbox2:checked + .checkbox-label:after{ 170 | color:#f35f42; 171 | } 172 | /* BLUE CHECKBOX */ 173 | 174 | #checkbox3 + .checkbox-label{ 175 | /*box-shadow*/ 176 | -webkit-box-shadow:inset 0 0 0 0px #1fc1c8,0 0 0 2px #dddddd; 177 | -moz-box-shadow:inset 0 0 0 0px #1fc1c8,0 0 0 2px #dddddd; 178 | box-shadow:inset 0 0 0 0px #1fc1c8,0 0 0 2px #dddddd; 179 | } 180 | #checkbox3:checked + .checkbox-label{ 181 | /*box-shadow*/ 182 | -webkit-box-shadow:inset 0 0 0 20px #1fc1c8,0 0 0 2px #1fc1c8; 183 | -moz-box-shadow:inset 0 0 0 20px #1fc1c8,0 0 0 2px #1fc1c8; 184 | box-shadow:inset 0 0 0 20px #1fc1c8,0 0 0 2px #1fc1c8; 185 | } 186 | #checkbox3:checked + .checkbox-label:after{ 187 | color:#1fc1c8; 188 | } 189 | 190 | 191 | -------------------------------------------------------------------------------- /config/index.html: -------------------------------------------------------------------------------- 1 | Hello world 2 | -------------------------------------------------------------------------------- /config/pw2py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Plugwise-2.py application 33 | 34 | 35 |
36 |
37 |
38 |
40 |
41 | 42 |
{{circle.name}} - {{circle.location}}
43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |
Switch 52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
Schedule 64 | 65 | 66 |
{{circle.schedule}}
67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 |
76 | 77 | {{circle.power}} W
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | 90 | 99 | 100 |
101 | 102 |
103 | 104 | -------------------------------------------------------------------------------- /config/schedules/carcharging.json: -------------------------------------------------------------------------------- 1 | {"name":"autoladen","description":"carcharging during night time","schedule":[[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,10,10],[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,10,10],[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,10,10],[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,10,10],[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,10,10],[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,-1,-1,10,10],[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,-1,-1,10,10]]} -------------------------------------------------------------------------------- /config/schedules/testsched1.json: -------------------------------------------------------------------------------- 1 | {"name":"Switching schedule 1","schedule":[[0,0,0,0,0,-1,0,0,0,0,0,0,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,-1,10,100,1000,0,-1,0,0,0,0],[0,0,0,0,0,0,-1,0,0,0,0,0,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,-1,0,0,0],[0,0,0,0,0,0,0,-1,0,0,0,0,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,-1,0,0,0,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,-1,0,0,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,-1,0,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,-1,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,5,10,20,40,100,200,400,1000,1500,2000,2500,3000,0,0,0,0,0,0,0,0,0,0,0]],"description":"standby killer test schedule"} -------------------------------------------------------------------------------- /config/schedules/testsched2.json: -------------------------------------------------------------------------------- 1 | {"name":"Switching schedule 1","schedule":[[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,0,0,0,0,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,15,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,0,0,0,0,0,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,15,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,-1,0,0,0,0,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,15,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,-1,0,0,0,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,15,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,0,-1,0,0,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,15,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,0,0,-1,0,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,15,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,0,0,0,-1,0,-1,2,7,15,50,200,1000,0,-1,7,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,5,7,15,50,200,1000,0,-1,10,10,10,50,200,1000,0,-1,5,5,5,5,5,1000,0,-1,-1,-1,-1,-1,-1,-1,-1]],"description":"standby killer test schedule"} -------------------------------------------------------------------------------- /config/schedules/winter.json: -------------------------------------------------------------------------------- 1 | {"name":"Switching schedule 1","schedule":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],[-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0]],"description":"Lichten vakantie"} -------------------------------------------------------------------------------- /devtools/plugwsie_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2012,2013,2014,2015,2016,2017,2018,2019,2020 Seven Watt 4 | # 5 | # 6 | # This file is part of Plugwise-2-py. 7 | # 8 | # Plugwise-2-py is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Plugwise-2-py is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Plugwise-2-py. If not, see . 20 | # 21 | # The program is a major modification and extension to: 22 | # python-plugwise - written in 2011 by Sven Petai 23 | # which itself is inspired by Plugwise-on-Linux (POL): 24 | # POL v0.2 - written in 2009 by Maarten Damen 25 | 26 | from pprint import pprint 27 | import optparse 28 | import logging 29 | 30 | from serial.serialutil import SerialException 31 | 32 | from plugwise import * 33 | from swutil.util import * 34 | from plugwise.api import * 35 | 36 | log_comm(False) 37 | init_logger("plugwise_util.log") 38 | log_level(logging.INFO) 39 | 40 | DEFAULT_SERIAL_PORT = "/dev/ttyUSB0" 41 | 42 | #default settings for attr-field of cirle 43 | conf = {"mac": "000D6F0001000000", "category": "misc", "name": "circle_n", "loginterval": "60", "always_on": "False", "production": "False", "reverse_pol": "False", "location": "misc"} 44 | 45 | parser = optparse.OptionParser() 46 | parser.add_option("-m", "--mac", dest="mac", help="MAC address") 47 | parser.add_option("-d", "--device", dest="device", 48 | help="Serial port device") 49 | parser.add_option("-p", "--power", action="store_true", 50 | help="Get current power usage") 51 | parser.add_option("-t", "--time", dest="time", 52 | help="""Set circle's internal clock to given time. 53 | Format is 'YYYY-MM-DD hh:mm:ss' use the special value 'sync' if you want to set Circles clock to the same time as your computer""") 54 | parser.add_option("-C", "--counter", action="store_true", 55 | help="Print out values of the pulse counters") 56 | parser.add_option("-c", "--continuous", type="int", 57 | help="Perform the requested action in an endless loop, sleeping for the given number of seconds in between.") 58 | parser.add_option("-s", "--switch", dest="switch", 59 | help="Switch power on/off. Possible values: 1,on,0,off") 60 | parser.add_option("-l", "--log", dest="log", 61 | help="""Read power usage history from the log buffers of the Circle. 62 | Argument should be 'cur' or 'current' if you want to read the log buffer that is currently being written. 63 | It can also be a numeric log buffer index if you want to read an arbitrary log buffer. 64 | """) 65 | parser.add_option("-i", "--info", action="store_true", dest="info", 66 | help="Perform the info request") 67 | parser.add_option("-q", "--query", dest="query", 68 | help="""Query data. Possible values are: time, pulses, last_logaddr, relay_state""") 69 | parser.add_option("-v", "--verbose", dest="verbose", 70 | help="""Verbose mode. Argument should be a number representing verboseness. 71 | Currently all the debug is logged at the same level so it doesn't really matter which number you use.""") 72 | 73 | options, args = parser.parse_args() 74 | 75 | device = DEFAULT_SERIAL_PORT 76 | 77 | if options.device: 78 | device = options.device 79 | 80 | if not options.mac: 81 | print("you have to specify mac with -m") 82 | parser.print_help() 83 | sys.exit(-1) 84 | 85 | if options.verbose: 86 | plugwise.util.DEBUG_PROTOCOL = True 87 | 88 | def print_pulse_counters(c): 89 | try: 90 | print("%d %d %d %d" % c.get_pulse_counters()) 91 | except ValueError: 92 | print("Error: Failed to read pulse counters") 93 | 94 | def handle_query(c, query): 95 | if query == 'time': 96 | print(c.get_clock().strftime("%H:%M:%S")) 97 | elif query == 'pulses': 98 | print_pulse_counters(c) 99 | elif query in ('last_logaddr', 'relay_state'): 100 | print(c.get_info()[query]) 101 | 102 | def handle_log(c, log_opt): 103 | if log_opt in ('cur', 'current'): 104 | log_idx = None 105 | else: 106 | try: 107 | log_idx = int(log_opt) 108 | except ValueError: 109 | print("log option argument should be either number or string current") 110 | return False 111 | 112 | print("power usage log:") 113 | for dt, watt, watt_hours in c.get_power_usage_history(log_idx): 114 | 115 | if dt is None: 116 | ts_str, watt, watt_hours = "N/A", "N/A", "N/A" 117 | else: 118 | ts_str = dt.strftime("%Y-%m-%d %H:%M") 119 | watt = "%7.2f" % (watt,) 120 | watt_hours = "%7.2f" % (watt_hours,) 121 | 122 | print("\t%s %s W %s Wh" % (ts_str, watt, watt_hours)) 123 | 124 | return True 125 | 126 | 127 | def set_time(c, time_opt): 128 | if time_opt == 'sync': 129 | set_ts = datetime.datetime.now() 130 | else: 131 | try: 132 | set_ts = datetime.datetime.strptime(time_opt, "%Y-%m-%d %H:%M:%S") 133 | except ValueError as reason: 134 | print("Error: Could not parse the time value: %s" % (str(reason),)) 135 | sys.exit(-1) 136 | 137 | c.set_clock(set_ts) 138 | 139 | try: 140 | device = Stick(device) 141 | 142 | conf['mac'] = options.mac.upper() 143 | c = Circle(conf['mac'], device, conf) 144 | 145 | 146 | if options.time: 147 | set_time(c, options.time) 148 | 149 | if options.switch: 150 | sw_direction = options.switch.lower() 151 | 152 | if sw_direction in ('on', '1'): 153 | c.switch_on() 154 | elif sw_direction in ('off', '0'): 155 | c.switch_off() 156 | else: 157 | print("Error: Unknown switch direction: "+sw_direction) 158 | sys.exit(-1) 159 | 160 | while 1: 161 | if options.power: 162 | try: 163 | print("power usage: 1s=%7.2f W 8s=%7.2f W 1h=%7.2f W 1h=%7.2f W(production)" % c.get_power_usage()) 164 | except ValueError: 165 | print("Error: Failed to read power usage") 166 | 167 | if options.log != None: 168 | handle_log(c, options.log) 169 | 170 | if options.info: 171 | print("info:") 172 | pprint(c.get_info()) 173 | 174 | if options.counter: 175 | print_pulse_counters(c) 176 | 177 | if options.query: 178 | handle_query(c, options.query) 179 | 180 | if options.continuous is None: 181 | break 182 | else: 183 | time.sleep(options.continuous) 184 | 185 | except (TimeoutException, SerialException) as reason: 186 | print("Error: %s" % (reason,)) 187 | -------------------------------------------------------------------------------- /domoticz/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | -------------------------------------------------------------------------------- /domoticz/README.md: -------------------------------------------------------------------------------- 1 | Plugwise-2-py - Domoticz bridge 2 | =============================== 3 | 4 | #Installation 5 | - Make sure NodeRed is installed. See Domoticz WIKI. 6 | - In Domoticz a (hardware) MQTT gateway needs to be defined. See Domoticz WIKI 7 | - In Domoticz create a Dummy hardware device and call it "VirtualPlugwise-2-py". Make a note of its hardware id. 8 | - Copy the content of plugwise2py-domoticz.nodered to NodeRed. Top-right corner in NodeRed webpage, select Import -> Clipboard. Paste the text. 9 | - MQTT defaults to 127.0.0.1:1883. If needed enter the right MQTT broker into the three MQTT related nodes in NodeRed. 10 | - Edit the first few lines in the node "initialise global context". The default ip and port of domoticz might be right, but the HW id of the Dummy hardware for the virtual switches/sensors needs to be entered. 11 | -------------------------------------------------------------------------------- /domoticz/plugwise2py-domoticz.nodered: -------------------------------------------------------------------------------- 1 | [{"id":"7eff7bb2.810084","type":"mqtt-broker","broker":"127.0.0.1","port":"1883","clientid":""},{"id":"242f10e5.dbd0f","type":"function","name":"Plugwise2 Energy to Domoticz","func":"//\n// Relay Plugwise MQTT energy message to Domoticz HTTP\n//\nnode.log (\"Relay Plugwise MQTT energy message to Domoticz HTTP\");\nvar sensor_url = context.global.plugwiseCfg.sensor_base_url;\nvar pwJSON = JSON.parse(msg.payload);\nvar state = context.global.plugwiseState[pwJSON.mac];\nvar pvalue = pwJSON.power;\nvar evalue = pwJSON.cum_energy;\nif (state)\n if (state.state.production) {\n pvalue = -pvalue;\n evalue = -evalue;\n }\n\n///json.htm?type=command¶m=udevice&hid=$HID&did=$DID&dunit=$DUNIT&dtype=$DTYPE&dsubtype=$DSUBTYPE&nvalue=$NVALUE&svalue=$SVALUE\"\nmsg.url = sensor_url + \"did=E\"+pwJSON.mac.substr(9,7)+\"&dunit=1&dtype=243&dsubtype=29&nvalue=&svalue=\"+pvalue+\";\"+evalue;\nreturn msg;","outputs":"1","noerr":0,"x":497,"y":107,"z":"77aa3b96.8855c4","wires":[["4a7e97b1.b58168","a4aef440.5b5108"]]},{"id":"44f23689.bb0dc8","type":"function","name":"Plugwise2 Power to Domoticz","func":"//\n// Relay Plugwise MQTT power message to Domoticz HTTP\n//\nnode.log (\"Relay Plugwise MQTT power message to Domoticz HTTP\");\nvar sensor_url = context.global.plugwiseCfg.sensor_base_url;\nvar pwJSON = JSON.parse(msg.payload);\n\nvar state = context.global.plugwiseState[pwJSON.mac];\nvar value = pwJSON.power;\nif (state)\n if (state.state.production)\n value = -value;\n\n///json.htm?type=command¶m=udevice&idx=$HID&did=$DID&dunit=$DUNIT&dtype=$DTYPE&dsubtype=$DSUBTYPE&nvalue=$NVALUE&svalue=$SVALUE\"\nmsg.url = sensor_url + \"did=F\"+pwJSON.mac.substr(9,7)+\"&dunit=1&dtype=248&dsubtype=1&nvalue=&svalue=\"+value+\";0\";\n\nreturn msg;","outputs":"1","noerr":0,"x":495,"y":173,"z":"77aa3b96.8855c4","wires":[["4a7e97b1.b58168","bc90e40a.436f18"]]},{"id":"4528a08f.bad76","type":"mqtt out","name":"MQTT publish","topic":"","qos":"","retain":"","broker":"7eff7bb2.810084","x":862,"y":327.75,"z":"77aa3b96.8855c4","wires":[]},{"id":"883b9d9b.77c46","type":"function","name":"Plugwise2 State to Domoticz","func":"//\n// Relay Plugwise MQTT state/circle message to Domoticz HTTP\n//\nnode.log (\"Relay Plugwise MQTT state/circle message to Domoticz HTTP\");\nvar switch_url = context.global.plugwiseCfg.switch_base_url;\nvar sensor_url = context.global.plugwiseCfg.sensor_base_url;\nvar pwJSON = JSON.parse(msg.payload);\nvar pwSwitch = 0;\nif (pwJSON.switch.toLowerCase() == \"on\")\n pwSwitch = 1;\nvar pwSchedule = 0;\nif (pwJSON.schedule.toLowerCase() == \"on\")\n pwSchedule = 1;\nvar info = \"\";\nif (pwJSON.schedname.length > 0)\n info += \"schedule: \" + pwJSON.schedname;\nif (!pwJSON.online) {\n if (info.length > 0)\n info += \", \";\n info += \"offline\";\n}\n\n///json.htm?type=command¶m=udevice&idx=$HID&did=$DID&dunit=$DUNIT&dtype=$DTYPE&dsubtype=$DSUBTYPE&nvalue=$NVALUE&svalue=$SVALUE\"\nvar switch_msg = {};\nvar schedule_msg = {};\nvar status_msg = {};\nswitch_msg.url = switch_url + \"did=B\"+pwJSON.mac.substr(9,7)+\"&dunit=1&dtype=17&dsubtype=0&nvalue=\" + pwSwitch + \"&svalue=15\";\nschedule_msg.url = switch_url + \"did=C\"+pwJSON.mac.substr(9,7)+\"&dunit=1&dtype=17&dsubtype=0&nvalue=\" + pwSchedule + \"&svalue=15\";\nstatus_msg.url = sensor_url + \"did=A\"+pwJSON.mac.substr(9,7)+\"&dunit=1&dtype=243&dsubtype=19&nvalue=&svalue=\"+info;\n\n//discard state messages as reply to a domoticz switch command\nif (pwJSON.requid == context.global.plugwiseReqUID + 0) {\n context.global.plugwiseState[pwJSON.mac] = { ts: Date.now(), state: pwJSON, update: \"schedule\" };\n return [null, schedule_msg, status_msg];\n}\nelse if (pwJSON.requid == context.global.plugwiseReqUID + 1) {\n context.global.plugwiseState[pwJSON.mac] = { ts: Date.now(), state: pwJSON, update: \"switch\" };\n return [switch_msg, null, status_msg];\n}\ncontext.global.plugwiseState[pwJSON.mac] = { ts: Date.now(), state: pwJSON, update: \"both\" };\n\n//return [msg, context.global.plugwiseState[pwJSON.mac]];\nreturn [switch_msg, schedule_msg, status_msg];\n\n\n\n","outputs":"3","noerr":0,"x":492,"y":238,"z":"77aa3b96.8855c4","wires":[["4a7e97b1.b58168","8b698c5b.74967"],["4a7e97b1.b58168","1b7f5a48.e480a6"],["4a7e97b1.b58168","30d088d.fcf2f78"]]},{"id":"bc90e40a.436f18","type":"debug","name":"msg.url","active":false,"console":"true","complete":"url","x":885,"y":142,"z":"77aa3b96.8855c4","wires":[]},{"id":"4a7e97b1.b58168","type":"http request","name":"Domoticz","method":"use","ret":"txt","url":"","x":733,"y":173,"z":"77aa3b96.8855c4","wires":[[]]},{"id":"b2b3cdc5.4d4c3","type":"inject","name":"run at startup","topic":"","payload":"","payloadType":"none","repeat":"","crontab":"","once":true,"x":104,"y":41,"z":"77aa3b96.8855c4","wires":[["ded739de.2128c8"]]},{"id":"ded739de.2128c8","type":"function","name":"Initialise global context","func":"//edit the following variables to match your configuration:\ndomoticz_ip = \"127.0.0.1\";\ndomoticz_port = \"8080\";\nvirtual_hardware_id = 3;\nvirtual_switch_id = 3;\n//end config section\n\n//initialize plugwise-domoticz bridge global variables\nif (!context.global.plugwiseCfg) {\n context.global.plugwiseCfg = {};\n}\nif (!context.global.plugwiseState) {\n context.global.plugwiseState = [];\n}\nif (!context.global.plugwiseReqUID) {\n context.global.plugwiseReqUID = Date.now().toString();\n}\n\ncontext.global.plugwiseCfg.sensor_base_url = \"http://\" + domoticz_ip + \":\" + domoticz_port + \"/json.htm?type=command¶m=udevice&idx=\" + virtual_hardware_id +\"&\";\ncontext.global.plugwiseCfg.switch_base_url = \"http://\" + domoticz_ip + \":\" + domoticz_port + \"/json.htm?type=command¶m=udevice&idx=\" + virtual_switch_id +\"&\";\n\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":41,"z":"77aa3b96.8855c4","wires":[[]]},{"id":"e0d70468.1f28f8","type":"mqtt in","name":"domoticz/out/#","topic":"domoticz/out/#","broker":"7eff7bb2.810084","x":81.5,"y":328,"z":"77aa3b96.8855c4","wires":[["2b9f2296.d460de"]]},{"id":"9d174f2e.62e8b","type":"switch","name":"Lighting_2","property":"dtype","rules":[{"t":"eq","v":"Lighting 2"}],"checkall":"true","outputs":1,"x":184.5,"y":427,"z":"77aa3b96.8855c4","wires":[["7bd5c5a.f842a3c"]]},{"id":"7bd5c5a.f842a3c","type":"switch","name":"AC","property":"stype","rules":[{"t":"eq","v":"AC"}],"checkall":"true","outputs":1,"x":331.5,"y":427,"z":"77aa3b96.8855c4","wires":[["44cfed0d.bb3014"]]},{"id":"2c6ead94.d39152","type":"debug","name":"mqtt topic","active":true,"console":"true","complete":"topic","x":850.5,"y":394,"z":"77aa3b96.8855c4","wires":[]},{"id":"2b9f2296.d460de","type":"function","name":"de-jsonify","func":"var payload = JSON.parse(msg.payload);\nreturn payload;","outputs":1,"noerr":0,"x":233,"y":328,"z":"77aa3b96.8855c4","wires":[["9d174f2e.62e8b"]]},{"id":"44cfed0d.bb3014","type":"switch","name":"svalue1=0","property":"svalue1","rules":[{"t":"eq","v":"0"}],"checkall":"true","outputs":1,"x":482,"y":427,"z":"77aa3b96.8855c4","wires":[["36f29dd7.c90d62"]]},{"id":"36f29dd7.c90d62","type":"function","name":"Domoticz switch/schedule to plugwise","func":"// Switch on and off the plugwise module\nnode.log (\"Relay Domoticz http:// requests to Plugwise2py MQTT\");\n\nvar pwmsg = {\"payload\": {}};\n//msg.payload.mac = \"000D6F000\"+msg.payload.mac;\n//strip leading 'B' (=breaker-switch) or 'C' (=clock-timer-schedule) and add prefix\nif (msg.id.length != 8)\n return;\npwmsg.payload.mac = \"000D6F000\"+msg.id.substr(1);\nvar type;\nif (msg.id[0] == 'B') {\n type = \"switch\";\n pwmsg.payload.uid = context.global.plugwiseReqUID + 0;\n} else {\n type = \"schedule\";\n pwmsg.payload.uid = context.global.plugwiseReqUID + 1;\n}\npwmsg.payload.cmd = type;\nif (msg.nvalue === 0)\n pwmsg.payload.val = \"off\";\nelse\n pwmsg.payload.val = \"on\";\n\n\npwmsg.topic = \"plugwise2py/cmd/\" + type + \"/\" + pwmsg.payload.mac;\n\nreturn pwmsg;","outputs":"1","noerr":0,"x":521,"y":328,"z":"77aa3b96.8855c4","wires":[["2c6ead94.d39152","4528a08f.bad76","e108f1e3.1ef71"]]},{"id":"ea1c984d.15e368","type":"switch","name":"","property":"topic","rules":[{"t":"cont","v":"energy"},{"t":"cont","v":"power"},{"t":"cont","v":"circle"}],"checkall":"true","outputs":3,"x":268,"y":173,"z":"77aa3b96.8855c4","wires":[["242f10e5.dbd0f"],["44f23689.bb0dc8"],["883b9d9b.77c46"]]},{"id":"7a303d11.85cfc4","type":"mqtt in","name":"","topic":"plugwise2py/state/#","broker":"7eff7bb2.810084","x":99,"y":173,"z":"77aa3b96.8855c4","wires":[["ea1c984d.15e368"]]},{"id":"e108f1e3.1ef71","type":"debug","name":"mqtt payload","active":true,"console":"true","complete":"payload","x":861,"y":444,"z":"77aa3b96.8855c4","wires":[]},{"id":"30d088d.fcf2f78","type":"debug","name":"msg.url","active":false,"console":"true","complete":"url","x":885,"y":260,"z":"77aa3b96.8855c4","wires":[]},{"id":"1b7f5a48.e480a6","type":"debug","name":"msg.url","active":true,"console":"true","complete":"url","x":884,"y":221,"z":"77aa3b96.8855c4","wires":[]},{"id":"8b698c5b.74967","type":"debug","name":"msg.url","active":true,"console":"true","complete":"url","x":883,"y":182,"z":"77aa3b96.8855c4","wires":[]},{"id":"a4aef440.5b5108","type":"debug","name":"msg.url","active":false,"console":"true","complete":"url","x":885,"y":105,"z":"77aa3b96.8855c4","wires":[]}] 2 | -------------------------------------------------------------------------------- /homey/README.md: -------------------------------------------------------------------------------- 1 | Plugwise-2-py - homey installation 2 | ================================== 3 | 4 | #Installation 5 | After installing en configuring Plugwise-2-py install the Homey app through 6 | the appstore: https://apps.athom.com/app/com.gruijter.plugwise2py 7 | 8 | For further instructions please visit https://forum.athom.com/discussion/1998 9 | -------------------------------------------------------------------------------- /openhab/README.md: -------------------------------------------------------------------------------- 1 | Plugwise-2-py Openhab integration 2 | ================================= 3 | Openhab can communicate with Plugwise-2-py through a MQTT server. Openhab provides a convenient system to operate switches and schedules. Also it can be used to record power readings and draw some graphs. 4 | 5 | TODO: Add a description. 6 | Example sitemap, items, rules and transforms can be found in the openhab folder in this repository 7 | 8 | -------------------------------------------------------------------------------- /openhab/configurations/items/plugwise2py.items: -------------------------------------------------------------------------------- 1 | Group All 2 | Group gCtrl (All) 3 | Group gState (All) 4 | Group gPowr (All) 5 | Group gProd (All) 6 | 7 | Group PWSwitch (All) 8 | Group PWState (All) 9 | Group PWMoni (All) 10 | 11 | //Group gLights (All, gCtrl) 12 | 13 | /* active groups */ 14 | /* 15 | Group:Switch:OR(ON, OFF) gLights "All Lights [(%d)]" (All,gCtrl) 16 | */ 17 | 18 | /***********************************************************************************************/ 19 | /* Plugwise switch section */ 20 | /***********************************************************************************************/ 21 | /* Switch sending switch command, and responding to status update message */ 22 | Switch SWI_Corridor "Corridor" (PWSwitch, gCtrl) 23 | {mqtt=" >[sevenw:plugwise2py/cmd/switch/000D6F0001A_mac1:command:*:JS(pw2py-cmdswitch.js)], 24 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac1:state:JS(pw2py-switch.js)]"} 25 | 26 | /* Power display item, which can request updates through commands issued in a rule */ 27 | String POW_Corridor "Corridor [%s]" (PWState, gCtrl) 28 | {mqtt=" >[sevenw:plugwise2py/cmd/reqstate/000D6F0001A_mac1:command:*:JS(pw2py-reqstate.js)], 29 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac1:state:JS(pw2py-power8s-s.js)]"} 30 | 31 | /* Switch state feedback reflecting the actual switch state with on/off icon */ 32 | String STA_Corridor "Corridor [%s]" (gCtrl) 33 | {mqtt=" <[sevenw:plugwise2py/state/circle/000D6F0001A_mac1:state:JS(pw2py-circleonoff.js)]"} 34 | 35 | Switch SWI_Circle1 "Circle 1" (PWSwitch, gCtrl) 36 | {mqtt=" >[sevenw:plugwise2py/cmd/switch/000D6F0001A_mac2:command:*:JS(pw2py-cmdswitch.js)], 37 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac2:state:JS(pw2py-switch.js)]"} 38 | 39 | /* Power display item, which can request updates through commands issued in a rule */ 40 | String POW_Circle1 "Circle 1 [%s]" (PWState, gCtrl) 41 | {mqtt=" >[sevenw:plugwise2py/cmd/reqstate/000D6F0001A_mac2:command:*:JS(pw2py-reqstate.js)], 42 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac2:state:JS(pw2py-power8s-s.js)]"} 43 | 44 | 45 | 46 | /***********************************************************************************************/ 47 | /* Solar PV production section */ 48 | /***********************************************************************************************/ 49 | /* Power monitor acting on 10-seconds continuous logging by plugwise2py */ 50 | Number MON_PV1 "Solar PV-1 Power [%.1f W]" (gProd) 51 | {mqtt=" <[sevenw:plugwise2py/state/power/000D6F0001A_mac3:state:JS(pw2py-monitor-prod.js)]"} 52 | Number MON_PV2 "Solar PV-2 Power [%.1f W]" (gProd) 53 | {mqtt=" <[sevenw:plugwise2py/state/power/000D6F0001A_mac4:state:JS(pw2py-monitor-prod.js)]"} 54 | 55 | /* Average power logging form the 60 (1-60) minutes Circle memory buffers */ 56 | Number AVG_PV1 "Solar PV-1 Avg Power [%.1f W]" (gProd) 57 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac3:state:JS(pw2py-avgpower-prod.js)]"} 58 | Number AVG_PV2 "Solar PV-2 Avg Power [%.1f W]" (gProd) 59 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac4:state:JS(pw2py-avgpower-prod.js)]"} 60 | 61 | /* Total energy production for today. Integrated through rule*/ 62 | Number ENE_INDAY_PV1 "Solar PV-1 Energy today [%.4f kWh]" (gProd) 63 | Number ENE_INDAY_PV2 "Solar PV-2 Energy today [%.4f kWh]" (gProd) 64 | 65 | /* Total energy production for today. Integrated through rule*/ 66 | Number ENE_DAY_PV1 "Solar PV-1 Energy yesterday [%.4f kWh]" (gProd) 67 | Number ENE_DAY_PV2 "Solar PV-2 Energy yesterday [%.4f kWh]" (gProd) 68 | 69 | /* Energy produced in last interval from Circle buffers. MQTT connection. Not displayed.*/ 70 | Number ENE_PV1 "Solar PV-1 Energy today [%.4f kWh]" 71 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac3:state:JS(pw2py-energy.js)]"} 72 | Number ENE_PV2 "Solar PV-2 Energy today [%.4f kWh]" 73 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac4:state:JS(pw2py-energy.js)]"} 74 | 75 | 76 | /***********************************************************************************************/ 77 | /* Energy consumption section */ 78 | /***********************************************************************************************/ 79 | Number MON_Corridor "Corridor Power [%.1f W]" (PWMoni, gPowr) 80 | {mqtt=" >[sevenw:plugwise2py/cmd/reqstate/000D6F0001A_mac1:command:*:JS(pw2py-reqstate.js)], 81 | <[sevenw:plugwise2py/state/power/000D6F0001A_mac1:state:JS(pw2py-monitor.js)], 82 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac1:state:JS(pw2py-power8s.js)]"} 83 | Number AVG_Corridor "Corridor Avg Power [%.1f W]" (gPowr) 84 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac1:state:JS(pw2py-avgpower.js)]"} 85 | Number ENE_INDAY_Corridor "Corridor Energy today [%.4f kWh]" (gPowr) 86 | Number ENE_DAY_Corridor "Corridor Energy yesterday [%.4f kWh]" (gPowr) 87 | Number ENE_Corridor "Corridor Energy [%.4f kWh]" 88 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac1:state:JS(pw2py-energy.js)]"} 89 | 90 | Number MON_Circle1 "Circle 1 Power [%.1f W]" (PWMoni, gPowr) 91 | {mqtt=" >[sevenw:plugwise2py/cmd/reqstate/000D6F0001A_mac2:command:*:JS(pw2py-reqstate.js)], 92 | <[sevenw:plugwise2py/state/power/000D6F0001A_mac2:state:JS(pw2py-monitor.js)], 93 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac2:state:JS(pw2py-power8s.js)]"} 94 | Number AVG_Circle1 "Circle 1 Avg Power [%.1f W]" (gPowr) 95 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac2:state:JS(pw2py-avgpower.js)]"} 96 | Number ENE_INDAY_Circle1 "Circle 1 Energy today [%.4f kWh]" (gPowr) 97 | Number ENE_DAY_Circle1 "Circle 1 Energy yesterday [%.4f kWh]" (gPowr) 98 | Number ENE_Circle1 "Circle 1 Energy [%.4f kWh]" 99 | {mqtt=" <[sevenw:plugwise2py/state/energy/000D6F0001A_mac2:state:JS(pw2py-energy.js)]"} 100 | 101 | 102 | /***********************************************************************************************/ 103 | /* Extended PW circle state examples */ 104 | /***********************************************************************************************/ 105 | /* Numerical Online-state item for graphs, which can request updates through commands issued in a rule */ 106 | Number ONL_Circle1 "Aqarium online: [%d]" (PWState, gState) 107 | {mqtt=" >[sevenw:plugwise2py/cmd/reqstate/000D6F0001A_mac2:command:*:JS(pw2py-reqstate.js)], 108 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac2:state:JS(pw2py-online-n.js)]"} 109 | 110 | /* Circle full state item, which can request updates through commands issued in a rule */ 111 | String INF_Circle1 "Circle 1 state: [%s]" (PWState, gState) 112 | {mqtt=" >[sevenw:plugwise2py/cmd/reqstate/000D6F0001A_mac2:command:*:JS(pw2py-reqstate.js)], 113 | <[sevenw:plugwise2py/state/circle/000D6F0001A_mac2:state:JS(pw2py-fullstate.js)]"} 114 | 115 | 116 | -------------------------------------------------------------------------------- /openhab/configurations/rules/plugwise2py.rules: -------------------------------------------------------------------------------- 1 | import org.openhab.core.library.types.* 2 | import org.openhab.core.persistence.* 3 | import org.openhab.model.script.actions.* 4 | import org.openhab.core.library.items.* 5 | import org.joda.time.* 6 | 7 | /* variables for switch */ 8 | var Timer switchinittimer 9 | var int initcnt 10 | var Timer switchedtimer1 11 | var Timer switchedtimer2 12 | 13 | /* variables for power and energy monitoring */ 14 | var Timer energyinittimer 15 | var int eninitcnt 16 | var DateTime PV1_last_dt = now.minusDays(1) 17 | var DateTime PV2_last_dt = now.minusDays(1) 18 | var DateTime Circle0_last_dt = now.minusDays(1) 19 | var DateTime Circle1_last_dt = now.minusDays(1) 20 | 21 | rule Start 22 | when 23 | System started 24 | then 25 | logInfo("Plugwise2py", "Plugwise rules loaded") 26 | end 27 | 28 | rule Stop 29 | when 30 | System shuts down 31 | then 32 | logInfo("Plugwise2py", "Plugwise exiting") 33 | end 34 | 35 | /***********************************************************************************************/ 36 | /* Plugwise switch section */ 37 | /* common rules */ 38 | /***********************************************************************************************/ 39 | rule "Plugwise2py initialize from Circles" 40 | when 41 | System started 42 | then 43 | //In the startup phase of openhab, sendCommands fail to result in sending a MQTT message 44 | //Therefore a retry timer resends the commands until a proper state is found for the circle. 45 | //PWState?.members.forEach(circle|logInfo("Plugwise2py", circle)) 46 | switchinittimer = createTimer(now.plusSeconds(10)) [| 47 | initcnt = 0 48 | PWState?.members.forEach(circle|logInfo("Plugwise2py", circle.toString)) 49 | PWState?.members.filter[state != Uninitialized].forEach(circle| { 50 | if (circle instanceof NumberItem) { 51 | val v = new Float(circle.state.toString) 52 | if ((v < -0.00005) && (v > -0.00015)) { 53 | circle.sendCommand(-0.0001) 54 | initcnt = initcnt + 1 55 | } 56 | } else if (circle.state.toString == "- W") { 57 | circle.sendCommand("- W") 58 | initcnt = initcnt + 1 59 | } 60 | } 61 | ) 62 | PWState?.members.filter[state == Uninitialized].forEach(circle| { 63 | if (circle instanceof NumberItem) {circle.sendCommand(-0.0001) } else {circle.sendCommand("- W")} 64 | initcnt = initcnt + 1 65 | } 66 | ) 67 | if (initcnt > 0) { 68 | switchinittimer.reschedule(now.plusSeconds(2)) 69 | logInfo("Plugwise2py", "Plugwise2py switch initialization sendCommand and rescheduled timer") 70 | } else { 71 | switchinittimer = null 72 | logInfo("Plugwise2py", "Plugwise2py switch initialization all online") 73 | } 74 | ] 75 | end 76 | 77 | rule "Periodic request plugwise state for switchable circles" 78 | when 79 | Time cron "3 * * * * ?" 80 | then 81 | //Send a command that actually is the items state. 82 | //The item will show the command value, which is the same as its state. 83 | PWState?.members.filter[state != Uninitialized].forEach(circle| circle.sendCommand(circle.state.toString)) 84 | PWState?.members.filter[state == Uninitialized].forEach(circle| { 85 | if (circle instanceof NumberItem) {circle.sendCommand(-0.0001) } else {circle.sendCommand("- W")} 86 | } 87 | ) 88 | end 89 | 90 | /***********************************************************************************************/ 91 | /* rules per switch */ 92 | /***********************************************************************************************/ 93 | //After switching the circle, it takes 8 seconds until the switch 94 | //has a proper power recording. Query state 10 seconds after switching. 95 | rule "Update circle state 10 seconds after switch" 96 | when 97 | //Item SWI_Corridor changed 98 | Item SWI_Corridor received command 99 | then 100 | switchedtimer1 = createTimer(now.plusSeconds(10)) [| 101 | //one of the following: 102 | POW_Corridor.sendCommand(POW_Corridor.state.toString) 103 | //STA_Corridor.sendCommand(STA_Corridor.state) 104 | ] 105 | end 106 | 107 | //After switching the circle, it takes 8 seconds until the switch 108 | //has a proper power recording. Query state 10 seconds after switching. 109 | rule "Update circle state 10 seconds after switch" 110 | when 111 | //Item SWI_Corridor changed 112 | Item SWI_Circle1 received command 113 | then 114 | switchedtimer2 = createTimer(now.plusSeconds(10)) [| 115 | //one of the following: 116 | POW_Circle1.sendCommand(POW_Circle1.state.toString) 117 | //STA_Corridor.sendCommand(STA_Corridor.state) 118 | ] 119 | end 120 | 121 | /***********************************************************************************************/ 122 | /* Solar PV production section */ 123 | /* Energy consumption section */ 124 | /***********************************************************************************************/ 125 | rule "Initialize from Circles" 126 | when 127 | System started 128 | then 129 | //In the startup phase of openhab, sendCommands fail to result in sending a MQTT message 130 | //Therefore a retry timer resends the commands until a proper state is found for the circle. 131 | energyinittimer = createTimer(now.plusSeconds(15)) [| 132 | eninitcnt = 0 133 | if (ENE_INDAY_PV1.state == Uninitialized) {ENE_INDAY_PV1.postUpdate(0); eninitcnt = eninitcnt + 1} 134 | if (ENE_INDAY_PV2.state == Uninitialized) {ENE_INDAY_PV2.postUpdate(0); eninitcnt = eninitcnt + 1} 135 | if (ENE_INDAY_Corridor.state == Uninitialized) {ENE_INDAY_Corridor.postUpdate(0); eninitcnt = eninitcnt + 1} 136 | if (ENE_INDAY_Circle1.state == Uninitialized) {ENE_INDAY_Circle1.postUpdate(0); eninitcnt = eninitcnt + 1} 137 | if (eninitcnt > 0) { 138 | energyinittimer.reschedule(now.plusSeconds(15)) 139 | logInfo("Plugwise2py", "Plugwise2py energy initialization sendCommand and rescheduled timer") 140 | } else { 141 | energyinittimer = null 142 | } 143 | ] 144 | end 145 | 146 | rule "Periodic request plugwise state for monitoring" 147 | when 148 | Time cron "27 0/2 * * * ?" 149 | then 150 | //Send a command that actually is the items state. 151 | //The item will show the command value, which is the same as its state. 152 | PWMoni?.members.filter[state != Uninitialized].forEach(circle| circle.sendCommand(circle.state.toString)) 153 | PWMoni?.members.filter[state == Uninitialized].forEach(circle| { 154 | if (circle instanceof NumberItem) {circle.sendCommand(-0.0001) } else {circle.sendCommand("- W")} 155 | } 156 | ) 157 | end 158 | 159 | 160 | 161 | /***********************************************************************************************/ 162 | /* rules per circle for power and energy consumption */ 163 | /***********************************************************************************************/ 164 | rule "PV1 Todays energy production" 165 | when 166 | Item ENE_PV1 received update 167 | then 168 | if (PV1_last_dt.getDayOfMonth != now.getDayOfMonth) { 169 | ENE_DAY_PV1.postUpdate(ENE_INDAY_PV1.state as DecimalType) 170 | ENE_INDAY_PV1.postUpdate(-ENE_PV1.state as DecimalType) 171 | } else { 172 | ENE_INDAY_PV1.postUpdate(ENE_INDAY_PV1.state as DecimalType - ENE_PV1.state as DecimalType) 173 | } 174 | PV1_last_dt = now 175 | end 176 | 177 | rule "PV2 Todays energy production" 178 | when 179 | Item ENE_PV2 received update 180 | then 181 | if (PV2_last_dt.getDayOfMonth != now.getDayOfMonth) { 182 | ENE_DAY_PV2.postUpdate(ENE_INDAY_PV2.state as DecimalType) 183 | ENE_INDAY_PV2.postUpdate(-ENE_PV2.state as DecimalType) 184 | } else { 185 | ENE_INDAY_PV2.postUpdate(ENE_INDAY_PV2.state as DecimalType - ENE_PV2.state as DecimalType) 186 | } 187 | PV2_last_dt = now 188 | end 189 | 190 | rule "Corridor Todays energy production" 191 | when 192 | Item ENE_Corridor received update 193 | then 194 | if (Circle0_last_dt.getDayOfMonth != now.getDayOfMonth) { 195 | ENE_DAY_Corridor.postUpdate(ENE_INDAY_Corridor.state as DecimalType) 196 | ENE_INDAY_Corridor.postUpdate(ENE_Corridor.state as DecimalType) 197 | } else { 198 | ENE_INDAY_Corridor.postUpdate(ENE_INDAY_Corridor.state as DecimalType + ENE_Corridor.state as DecimalType) 199 | } 200 | Circle0_last_dt = now 201 | end 202 | 203 | rule "Circle1 Todays energy production" 204 | when 205 | Item ENE_Circle1 received update 206 | then 207 | if (Circle1_last_dt.getDayOfMonth != now.getDayOfMonth) { 208 | ENE_DAY_Circle1.postUpdate(ENE_INDAY_Circle1.state as DecimalType) 209 | ENE_INDAY_Circle1.postUpdate(ENE_Circle1.state as DecimalType) 210 | } else { 211 | ENE_INDAY_Circle1.postUpdate(ENE_INDAY_Circle1.state as DecimalType + ENE_Circle1.state as DecimalType) 212 | } 213 | Circle1_last_dt = now 214 | end 215 | 216 | -------------------------------------------------------------------------------- /openhab/configurations/sitemaps/plugwise2py.sitemap: -------------------------------------------------------------------------------- 1 | sitemap Plugwise2py label="Plugwsie-2-py Control and Monitor" 2 | { 3 | Frame { 4 | Group item=gCtrl label="Plugwise control" icon="switch" 5 | Group item=gPowr label="Energy consumption" icon="energy" 6 | Group item=gProd label="Energy production" icon="sun" 7 | Group item=gState label="Circle state" icon="temperature" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-avgpower-prod.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | -json.power; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-avgpower.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | json.power; 3 | 4 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-circleonoff.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | ws = json["switch"]; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-cmdswitch.js: -------------------------------------------------------------------------------- 1 | result = '{"mac":"","cmd":"switch","val":"' + input.toLowerCase() + '"}' 2 | result; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-energy.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | json.energy / 1000.0; 3 | 4 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-fullstate.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | var s = ""; 3 | s += "switch: " + json["switch"].toUpperCase(); 4 | s += ", sched{"+ json.schedname + "}:" + json.schedule.toUpperCase(); 5 | s += ", type: " + json.type; 6 | s += ", name: " + json.location + "/" + json.name; 7 | s += ", " + (json.readonly ? "always ON" : ""); 8 | s += ", monitor: " + (json.monitor ? "yes" : "no"); 9 | s += ", log: " + (json.savelog ? "yes" : "no"); 10 | s += ", interval: " + json.interval + "m"; 11 | s += ", prod: " + (json.production ? "yes" : "no"); 12 | if (json.online) { 13 | // s = json.power8s.toFixed(1) + " W at "; 14 | // var date = new Date(1000 * json.powerts); 15 | // //s += date.toLocaleString(); 16 | // s += date.toLocaleDateString() + " " + date.toLocaleTimeString(); 17 | s += ", ONLINE"; 18 | } else { 19 | s += ", OFFLINE @ "; 20 | var date = new Date(1000 * json.lastseen); 21 | //s += date.toLocaleString(); 22 | s += date.toLocaleDateString() + " " + date.toLocaleTimeString(); 23 | } 24 | s; -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-monitor-prod.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | -json.power; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-monitor.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | json.power; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-online-n.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | json.online ? 1 : 0; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-power1h.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | json.power1h; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-power8s-s.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | //ws = json["switch"]; 3 | if (json.online) { 4 | ws = json.power8s.toFixed(1) + " W"; 5 | } else { 6 | ws = "offline"; 7 | } 8 | ws; 9 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-power8s.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | json.power8s; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-reqstate.js: -------------------------------------------------------------------------------- 1 | result = '{"mac":"","cmd":"reqstate","val":"' + input.toLowerCase() + '"}' 2 | result; 3 | -------------------------------------------------------------------------------- /openhab/configurations/transform/pw2py-switch.js: -------------------------------------------------------------------------------- 1 | var json = JSON.parse(input); 2 | ws = json["switch"].toUpperCase(); 3 | -------------------------------------------------------------------------------- /plugwise/__init__.py: -------------------------------------------------------------------------------- 1 | #An empty file is enough -------------------------------------------------------------------------------- /plugwise/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012,2013,2014,2015,2016,2017,2018,2019,2020 Seven Watt 2 | # 3 | # 4 | # This file is part of Plugwise-2-py. 5 | # 6 | # Plugwise-2-py is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Plugwise-2-py is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Plugwise-2-py. If not, see . 18 | # 19 | # The program is a major modification and extension to: 20 | # python-plugwise - written in 2011 by Sven Petai 21 | # which itself is inspired by Plugwise-on-Linux (POL): 22 | # POL v0.2 - written in 2009 by Maarten Damen 23 | 24 | class PlugwiseException(Exception): 25 | pass 26 | 27 | class ProtocolError(PlugwiseException): 28 | pass 29 | 30 | class TimeoutException(PlugwiseException): 31 | pass 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | 6 | VERSION = '2.0' 7 | 8 | install_reqs = ['crcmod', 'paho-mqtt', 'pyserial'] 9 | 10 | setup(name='plugwise2py', 11 | version=VERSION, 12 | description='A server to control and log readings form Plugwise devices.', 13 | author='Seven Watt', 14 | author_email='info@sevenwatt.com', 15 | url='https://github.com/SevenW/Plugwise-2-py', 16 | license='GPL', 17 | packages=find_packages(), 18 | py_modules=['plugwise'], 19 | install_requires=install_reqs, 20 | scripts=['Plugwise-2.py', 'Plugwise-2-web.py', 'swutil/util.py'], 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /swutil/HTTPWebSocketsHandler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2012,2013,2014,2015,2016,2017,2018,2019,2020 Seven Watt 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | ''' 13 | 14 | from http.server import SimpleHTTPRequestHandler 15 | import struct 16 | from base64 import b64encode 17 | from hashlib import sha1 18 | from io import StringIO 19 | import errno, socket #for socket exceptions 20 | import threading 21 | 22 | class WebSocketError(Exception): 23 | pass 24 | 25 | class HTTPWebSocketsHandler(SimpleHTTPRequestHandler): 26 | _ws_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 27 | _opcode_continu = 0x0 28 | _opcode_text = 0x1 29 | _opcode_binary = 0x2 30 | _opcode_close = 0x8 31 | _opcode_ping = 0x9 32 | _opcode_pong = 0xa 33 | # protocol_version = 'HTTP/1.1' where is this used? 34 | 35 | #mutex = threading.Lock() 36 | 37 | def on_ws_message(self, message): 38 | """Override this handler to process incoming websocket messages.""" 39 | pass 40 | 41 | def on_ws_connected(self): 42 | """Override this handler.""" 43 | pass 44 | 45 | def on_ws_closed(self): 46 | """Override this handler.""" 47 | pass 48 | 49 | def send_message(self, message): 50 | self._send_message(self._opcode_text, message) 51 | 52 | def setup(self): 53 | SimpleHTTPRequestHandler.setup(self) 54 | self.connected = False 55 | 56 | def finish(self): 57 | #needed when wfile is used, or when self.close_connection is not used 58 | 59 | # 60 | #catch errors in SimpleHTTPRequestHandler.finish() after socket disappeared 61 | #due to loss of network connection 62 | 63 | self.log_message("finish entry on worker thread %d" % threading.current_thread().ident) 64 | try: 65 | SimpleHTTPRequestHandler.finish(self) 66 | except (socket.error, TypeError) as err: 67 | self.log_message("finish(): Exception: in SimpleHTTPRequestHandler.finish(): %s" % str(err.args)) 68 | print(("finish(): Exception: in SimpleHTTPRequestHandler.finish(): %s" % str(err.args))) 69 | self.log_message("finish done on worker thread %d" % threading.current_thread().ident) 70 | 71 | def handle(self): 72 | #needed when wfile is used, or when self.close_connection is not used 73 | 74 | # 75 | #catch errors in SimpleHTTPRequestHandler.handle() after socket disappeared 76 | #due to loss of network connection 77 | 78 | self.log_message("handle entry on worker thread %d" % threading.current_thread().ident) 79 | try: 80 | SimpleHTTPRequestHandler.handle(self) 81 | except (socket.error,) as err: 82 | self.log_message("handle(): Exception: in SimpleHTTPRequestHandler.handle(): %s" % str(err.args)) 83 | except (TypeError,) as err: 84 | self.log_message("handle(): Exception: in SimpleHTTPRequestHandler.handle(): %s" % str(err)) 85 | self.log_message("handle done on worker thread %d" % threading.current_thread().ident) 86 | 87 | def checkAuthentication(self): 88 | auth = self.headers.get('Authorization') 89 | if auth != "Basic %s" % self.server.auth: 90 | self.send_response(401) 91 | self.send_header("WWW-Authenticate", 'Basic realm="Plugwise"') 92 | self.end_headers(); 93 | return False 94 | return True 95 | 96 | def do_GET(self): 97 | #self.log_message("HTTPWebSocketsHandler do_GET") 98 | if self.server.auth and not self.checkAuthentication(): 99 | return 100 | if self.headers.get("Upgrade", None) and self.headers.get("Upgrade", None).lower().strip() == "websocket": 101 | self.log_message("do_GET upgrade: headers:\r\n %s" % (str(self.headers),)) 102 | #self.log_message("do_GET upgrade: server %s" % (self.server,)) 103 | #self.log_message("do_GET upgrade: timeout1 %d" % (self.server.socket.gettimeout(),)) 104 | # if self.server.timeout != None: 105 | # self.log_message("do_GET upgrade: timeout2 %d" % (self.server.timeout,)) 106 | # else: 107 | # self.log_message("do_GET upgrade: timeout2 None") 108 | 109 | #self.server.socket.settimeout(0) 110 | 111 | self._handshake() 112 | #This handler is in websocket mode now. 113 | #do_GET only returns after client close or socket error. 114 | self._read_messages() 115 | else: 116 | SimpleHTTPRequestHandler.do_GET(self) 117 | 118 | def _read_messages(self): 119 | while self.connected == True: 120 | try: 121 | self._read_next_message() 122 | except (socket.error, WebSocketError) as e: 123 | #websocket content error, time-out or disconnect. 124 | self.log_message("RCV: Close connection: Socket Error %s" % str(e.args)) 125 | self._ws_close() 126 | except Exception as err: 127 | #unexpected error in websocket connection. 128 | self.log_error("RCV: Exception: in _read_messages: %s" % str(err.args)) 129 | self._ws_close() 130 | 131 | def _read_next_message(self): 132 | #self.rfile.read(n) is blocking. 133 | #it returns however immediately when the socket is closed. 134 | try: 135 | self.opcode = ord(self.rfile.read(1)) & 0x0F 136 | length = ord(self.rfile.read(1)) & 0x7F 137 | self.log_message("_read_next_message: opcode: %02X length: %d" % (self.opcode, length)) 138 | if length == 126: 139 | length = struct.unpack(">H", self.rfile.read(2))[0] 140 | elif length == 127: 141 | length = struct.unpack(">Q", self.rfile.read(8))[0] 142 | masks = [byte for byte in self.rfile.read(4)] 143 | decoded = "" 144 | for byte in self.rfile.read(length): 145 | decoded += chr(byte ^ masks[len(decoded) % 4]) 146 | self._on_message(decoded) 147 | except (struct.error, TypeError) as e: 148 | #catch exceptions from ord() and struct.unpack() 149 | if self.connected: 150 | self.log_message("_read_next_message exception %s" % (str(e.args))) 151 | raise WebSocketError("Websocket read aborted while listening") 152 | else: 153 | #the socket was closed while waiting for input 154 | self.log_error("RCV: _read_next_message aborted after closed connection") 155 | pass 156 | 157 | def _send_message(self, opcode, message): 158 | #self.log_message("_send_message: opcode: %02X msg: %s" % (opcode, message)) 159 | try: 160 | #use of self.wfile.write gives socket exception after socket is closed. Avoid. 161 | self.request.send((0x80 + opcode).to_bytes(1, 'big')) 162 | length = len(message) 163 | if length <= 125: 164 | self.request.send(length.to_bytes(1, 'big')) 165 | elif length >= 126 and length <= 65535: 166 | self.request.send((126).to_bytes(1, 'big')) 167 | self.request.send(struct.pack(">H", length)) 168 | else: 169 | self.request.send((127).to_bytes(1, 'big')) 170 | self.request.send(struct.pack(">Q", length)) 171 | if length > 0: 172 | self.request.send(message.encode('utf-8')) 173 | except socket.error as e: 174 | #websocket content error, time-out or disconnect. 175 | self.log_message("SND: Close connection: Socket Error %s" % str(e.args)) 176 | self._ws_close() 177 | except Exception as err: 178 | #unexpected error in websocket connection. 179 | self.log_error("SND: Exception: in _send_message: %s" % str(err.args)) 180 | self._ws_close() 181 | 182 | def _handshake(self): 183 | # self.log_message("_handshake()") 184 | headers=self.headers 185 | if self.headers.get("Upgrade", None) and self.headers.get("Upgrade", None).lower().strip() != "websocket": 186 | return 187 | key = headers['Sec-WebSocket-Key'] 188 | digest = b64encode(sha1((key + self._ws_GUID).encode()).digest()).decode() 189 | self.send_response(101, 'Switching Protocols') 190 | self.send_header('Upgrade', 'websocket') 191 | self.send_header('Connection', 'Upgrade') 192 | self.send_header('Sec-WebSocket-Accept', digest) 193 | self.end_headers() 194 | self.connected = True 195 | #self.close_connection = 0 196 | self.on_ws_connected() 197 | 198 | def _ws_close(self): 199 | #avoid closing a single socket two time for send and receive. 200 | #self.log_message("LOCK: WAIT _was_close obj: %s thd %s" % (str(self) , threading.current_thread().ident)) 201 | #self.mutex.acquire() 202 | #self.log_message("LOCK: ACKD _was_close obj: %s thd %s" % (str(self) , threading.current_thread().ident)) 203 | try: 204 | if self.connected: 205 | self.connected = False 206 | #Terminate BaseHTTPRequestHandler.handle() loop: 207 | self.close_connection = 1 208 | #send close and ignore exceptions. An error may already have occurred. 209 | try: 210 | self._send_close() 211 | except: 212 | self.log_message("_ws_close we send a close to a broken line. Do not expect much") 213 | pass 214 | self.on_ws_closed() 215 | else: 216 | self.log_message("_ws_close websocket in closed state. Ignore.") 217 | pass 218 | finally: 219 | #self.mutex.release() 220 | pass 221 | #self.log_message("LOCK: RLSD _was_close obj: %s thd %s" % (str(self) , threading.current_thread().ident)) 222 | 223 | def _on_message(self, message): 224 | # self.log_message("_on_message: opcode: %02X msg: %s" % (self.opcode, message)) 225 | 226 | # close 227 | if self.opcode == self._opcode_close: 228 | self.connected = False 229 | #Terminate BaseHTTPRequestHandler.handle() loop: 230 | self.close_connection = 1 231 | try: 232 | self._send_close() 233 | except: 234 | self.log_message("_on_message we send a close to a broken line. Do not expect much") 235 | pass 236 | self.on_ws_closed() 237 | # ping 238 | elif self.opcode == self._opcode_ping: 239 | _send_message(self._opcode_pong, message) 240 | # pong 241 | elif self.opcode == self._opcode_pong: 242 | pass 243 | # data 244 | elif (self.opcode == self._opcode_continu or 245 | self.opcode == self._opcode_text or 246 | self.opcode == self._opcode_binary): 247 | self.on_ws_message(message) 248 | 249 | def _send_close(self): 250 | #Dedicated _send_close allows for catch all exception handling 251 | msg = bytearray() 252 | msg.append(0x80 + self._opcode_close) 253 | msg.append(0x00) 254 | self.request.send(msg) 255 | 256 | -------------------------------------------------------------------------------- /swutil/__init__.py: -------------------------------------------------------------------------------- 1 | #An empty file is enough -------------------------------------------------------------------------------- /swutil/pwmqtt.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012,2013,2014,2015,2016,2017,2018,2019,2020 Seven Watt 2 | # 3 | # 4 | # This file is part of Plugwise-2-py. 5 | # 6 | # Plugwise-2-py is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Plugwise-2-py is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Plugwise-2-py. If not, see . 18 | # 19 | # The program is a major modification and extension to: 20 | # python-plugwise - written in 2011 by Sven Petai 21 | # which itself is inspired by Plugwise-on-Linux (POL): 22 | # POL v0.2 - written in 2009 by Maarten Damen 23 | 24 | import paho.mqtt.client as mosquitto 25 | from .util import * 26 | import queue 27 | import time 28 | import os 29 | 30 | class Mqtt_client(object): 31 | """Main program class 32 | """ 33 | def __init__(self, broker, port, qpub, qsub, name="pwmqtt", user = None, password = None): 34 | """ 35 | ... 36 | """ 37 | info("MQTT client initializing for " + str(broker) +":"+ str(port)) 38 | self.broker = str(broker) 39 | self.port = int(port) #str(port) 40 | self.qpub = qpub 41 | self.qsub = qsub 42 | self.rc = -1 43 | self.mqttc = None 44 | self.name = name+str(os.getpid()) 45 | self.user = user 46 | self.password = password 47 | self.subscriptions = {} 48 | 49 | self.connect() 50 | debug("MQTT init done") 51 | 52 | def connected(self): 53 | return (self.rc == 0) 54 | 55 | def connect(self): 56 | self.mqttc = mosquitto.Mosquitto(self.name) 57 | self.mqttc.on_message = self.on_message 58 | self.mqttc.on_connect = self.on_connect 59 | self.mqttc.on_disconnect = self.on_disconnect 60 | self.mqttc.on_publish = self.on_publish 61 | self.mqttc.on_subscribe = self.on_subscribe 62 | 63 | #Set the username and password if any 64 | if self.user != None: 65 | self.mqttc.username_pw_set(self.user,self.password) 66 | 67 | return self._connect() 68 | 69 | def _connect(self): 70 | try: 71 | self.rc = self.mqttc.connect(self.broker, self.port, 60) 72 | info("MQTT connected return code %d" % (self.rc,)) 73 | except Exception as reason: 74 | error("MQTT connection error: "+str(reason)) 75 | return self.rc 76 | 77 | def run(self): 78 | while True: 79 | while self.rc == 0: 80 | try: 81 | self.rc = self.mqttc.loop() 82 | except Exception as reason: 83 | self.rc = 1 84 | error("MQTT connection error in loop: "+str(reason)) 85 | continue; 86 | #process data to be published 87 | while not self.qpub.empty(): 88 | data = self.qpub.get() 89 | topic = data[0] 90 | msg = data[1] 91 | 92 | 93 | retain = False; 94 | if len(data)>2: 95 | retain = data[2] 96 | debug("MQTT publish: %s %s retain = %s" % (topic, msg, retain)) 97 | try: 98 | self.mqttc.publish(topic, msg, 0, retain) 99 | except Exception as reason: 100 | error("MQTT connection error in publish: " + str(reason)) 101 | time.sleep(0.1) 102 | error("MQTT disconnected") 103 | 104 | #attempt to reconnect 105 | time.sleep(5) 106 | self.rc = self._connect() 107 | if self.connected(): 108 | for subscr in list(self.subscriptions.items()): 109 | self.mqttc.subscribe(subscr[0], subscr[1]) 110 | info("MQTT subscribed to %s with qos %d" % (subscr[0], subscr[1])) 111 | 112 | 113 | def subscribe(self, topic, qos=0): 114 | self.subscriptions[topic] = qos 115 | if self.connected(): 116 | self.mqttc.subscribe(topic, qos) 117 | info("MQTT subscribed to %s with qos %d" % (topic, qos)) 118 | 119 | def unsubscribe(self, topic): 120 | self.subscriptions.pop(topic, None) 121 | if self.connected(): 122 | self.mqttc.unsubscribe(topic) 123 | info("MQTT unsubscribed from %s" % topic) 124 | 125 | def on_message(self, client, userdata, message): 126 | debug("MQTT " + message.topic+" "+str(message.payload)) 127 | self.qsub.put((message.topic, message.payload.decode('utf-8'))) 128 | 129 | def on_connect(self, client, userdata, flags, rc): 130 | if rc == 0: 131 | info("MQTT connected return code 0") 132 | else: 133 | error("MQTT connected return code %d" % (self.rc,)) 134 | self.rc = rc 135 | 136 | def on_disconnect(self, client, userdata, rc): 137 | self.rc = rc 138 | info("MQTT disconnected (from on_disconnect)") 139 | 140 | def on_publish(self, client, userdata, mid): 141 | debug("MQTT published message sequence number: "+str(mid)) 142 | 143 | def on_subscribe(self, client, userdata, mid, granted_qos): 144 | info("MQTT Subscribed: "+str(mid)+" "+str(granted_qos)) 145 | 146 | def on_unsubscribe(self, client, userdata, mid): 147 | info("MQTT Unsubscribed: "+str(mid)) 148 | 149 | # def on_log(self, client, userdata, level, buf): 150 | # info(buf) 151 | -------------------------------------------------------------------------------- /swutil/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012,2013,2014,2015,2016,2017,2018,2019,2020 Seven Watt 2 | # 3 | # 4 | # This file is part of Plugwise-2-py. 5 | # 6 | # Plugwise-2-py is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Plugwise-2-py is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Plugwise-2-py. If not, see . 18 | # 19 | # The program is a major modification and extension to: 20 | # python-plugwise - written in 2011 by Sven Petai 21 | # which itself is inspired by Plugwise-on-Linux (POL): 22 | # POL v0.2 - written in 2009 by Maarten Damen 23 | 24 | import sys 25 | import serial 26 | from serial.serialutil import SerialException 27 | import datetime 28 | import logging 29 | import logging.handlers 30 | 31 | LOG_COMMUNICATION = False 32 | 33 | #global var 34 | pw_logger = None 35 | pw_comm_logger = None 36 | 37 | def logf(msg): 38 | if type(msg) == type(" "): 39 | return msg 40 | if type(msg) == type(b' '): 41 | return repr(msg.decode('utf-8', 'backslashreplace'))[1:-1] 42 | return repr(msg)[1:-1] 43 | 44 | def hexstr(s): 45 | return ' '.join(hex(ord(x)) for x in s) 46 | 47 | def uint_to_int(val, octals): 48 | """compute the 2's compliment of int value val for negative values""" 49 | bits=octals<<2 50 | if( (val&(1<<(bits-1))) != 0 ): 51 | val = val - (1<