├── CONTRIBUTING.md ├── install ├── diag.sh ├── alpha │ ├── autoupdate.data │ ├── Python3 │ │ ├── autoupdate.data │ │ └── thermeq3.zip │ ├── thermeq3.zip │ ├── watch_bridge │ └── readme.md ├── current │ ├── autoupdate.data │ ├── thermeq3.zip │ └── config_me.py ├── RPi │ ├── thermeq3.zip │ ├── readme.md │ ├── diag.sh │ ├── install-dash.sh │ └── install.sh ├── dashboard │ ├── yun-dash.sh │ ├── README.md │ └── rpi-dash.sh └── uni_install.sh ├── obsolete ├── install │ ├── autoupdate.data │ ├── V231 │ │ ├── autoupdate.data │ │ ├── thermeq3.zip │ │ ├── config.py │ │ └── install.sh │ ├── alpha_Yun_Rev2 │ │ ├── readme.md │ │ └── install.sh │ ├── config.py │ ├── install.sh │ └── README.md ├── lib │ ├── nsm.py │ ├── action.py │ ├── public_ip.py │ ├── dummy.py │ ├── config.py │ ├── secweb.py │ ├── support.py │ ├── logmsg.py │ ├── csvfile.py │ ├── profiles.py │ ├── weather.py │ ├── autoupdate.py │ ├── mailer.py │ ├── bridge.py │ ├── t3_var.py │ └── maxeq3.py ├── create_nsm.sh ├── support │ └── sample temperature.py ├── tasker │ ├── parseyun.js │ └── parseyun-tasker.xml ├── recreate_http.sh └── betabeat │ └── html │ └── setup.html ├── sketch ├── Capture.PNG └── thermeq3.fzz ├── flowchart └── flowchart_1.png ├── support ├── readme.md ├── del_dev.py └── testrelay.py ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── eInk Display ├── README.md └── sketch │ └── exthermeq.ino ├── CODE_OF_CONDUCT.md ├── .gitignore ├── yun-sketch ├── thermeq3_V200 │ └── thermeq3_V200.ino ├── thermeq3 │ └── thermeq3.ino └── thermeq3_dht │ └── thermeq3_dht.ino └── README.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to test and comment. 2 | -------------------------------------------------------------------------------- /install/diag.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/install/diag.sh -------------------------------------------------------------------------------- /obsolete/install/autoupdate.data: -------------------------------------------------------------------------------- 1 | 155:nsm.py:60096:4ca7c6d25b4972de5166dabf959d9e8f 2 | -------------------------------------------------------------------------------- /install/alpha/autoupdate.data: -------------------------------------------------------------------------------- 1 | 309:thermeq3.zip:40541:0661c29dd8372d08ac7fc964fa88153e 2 | -------------------------------------------------------------------------------- /install/current/autoupdate.data: -------------------------------------------------------------------------------- 1 | 286:thermeq3.zip:35173:59233d1115b982d9c256aa91e411e97e 2 | -------------------------------------------------------------------------------- /sketch/Capture.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/sketch/Capture.PNG -------------------------------------------------------------------------------- /sketch/thermeq3.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/sketch/thermeq3.fzz -------------------------------------------------------------------------------- /install/alpha/Python3/autoupdate.data: -------------------------------------------------------------------------------- 1 | 300:thermeq3.zip:35474:00bd10c69a954b13ee79fe8925d7f35c 2 | -------------------------------------------------------------------------------- /obsolete/install/V231/autoupdate.data: -------------------------------------------------------------------------------- 1 | 231:thermeq3.zip:28394:c324f372cafaa93fc3d489a2cc052244 2 | -------------------------------------------------------------------------------- /install/RPi/thermeq3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/install/RPi/thermeq3.zip -------------------------------------------------------------------------------- /flowchart/flowchart_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/flowchart/flowchart_1.png -------------------------------------------------------------------------------- /install/alpha/thermeq3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/install/alpha/thermeq3.zip -------------------------------------------------------------------------------- /install/current/thermeq3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/install/current/thermeq3.zip -------------------------------------------------------------------------------- /install/alpha/Python3/thermeq3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/install/alpha/Python3/thermeq3.zip -------------------------------------------------------------------------------- /obsolete/install/V231/thermeq3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/obsolete/install/V231/thermeq3.zip -------------------------------------------------------------------------------- /obsolete/install/alpha_Yun_Rev2/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopower/thermeq3/HEAD/obsolete/install/alpha_Yun_Rev2/readme.md -------------------------------------------------------------------------------- /obsolete/lib/nsm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import thermeq3 3 | 4 | 5 | if __name__ == '__main__': 6 | thermeq3.start() 7 | thermeq3.loop() 8 | -------------------------------------------------------------------------------- /install/alpha/watch_bridge: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | if [ $(( (`date +%s` - `date -r /mnt/sda1/peklotest.bridge +%s`) > (5 * 60) )) -eq 1 ]; then 3 | ps -ef | grep nsm.py | grep -v grep | awk '{print }' | xargs kill -9 4 | fi 5 | -------------------------------------------------------------------------------- /obsolete/create_nsm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | echo "Creating nsm.py compatibility file" 3 | echo "#!/usr/bin/env python 4 | import sys 5 | 6 | sys.path.insert(0, \"/root/thermeq3/\") 7 | execfile(\"/root/thermeq3/nsm.py\") 8 | " > /root/nsm.py -------------------------------------------------------------------------------- /obsolete/install/config.py: -------------------------------------------------------------------------------- 1 | stp.max_ip = "192.168.0.10" 2 | stp.fromaddr = "devices@foo.local" 3 | stp.toaddr = "user@foo.local" 4 | stp.mailserver = "mail.foo.local" 5 | stp.mailport = 25 6 | stp.frompwd = "this.is.password" 7 | stp.devname = "hellmostat" 8 | stp.timeout = 10 9 | stp.api_key = "blablabla" -------------------------------------------------------------------------------- /obsolete/support/sample temperature.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def scale(val, src, dst): 4 | return ((val - src[0]) / (src[1]-src[0])) * (dst[1]-dst[0]) + dst[0] 5 | 6 | _min = 1.7 7 | _max = 3.0 8 | for each in range(-35, 36): 9 | a = scale(each, (0.0, 35.0), (_min, _max)) 10 | print "%3i" % each, "=", "% 2.4f" % a, "% 3i" % int(scale(math.exp(a), (math.exp(_min), math.exp(_max)), (15, 120))) 11 | 12 | -------------------------------------------------------------------------------- /obsolete/lib/action.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO 2 | 3 | 4 | def start(): 5 | GPIO.setwarnings(False) 6 | GPIO.setmode(GPIO.BCM) 7 | GPIO.setup(18, GPIO.OUT) 8 | # GPIO pin 18 is for DEV, prod is 17 9 | 10 | 11 | def do(action): 12 | if action: 13 | gpio_action = GPIO.HIGH 14 | else: 15 | gpio_action = GPIO.LOW 16 | try: 17 | GPIO.output(18, gpio_action) 18 | # PIN 18 if for DEV, 17 for PROD 19 | finally: 20 | pass 21 | -------------------------------------------------------------------------------- /support/readme.md: -------------------------------------------------------------------------------- 1 | ## Support code for thermeq3 2 | 3 | # test-relay.py 4 | This simple code test relay for RPi, check relay if it is NO or NC. 5 | Please run `python testrelay.py` and follow instructions. 6 | 7 | How to download: `wget --no-check-certificate --quiet --output-document /home/pi/testrelay.py https://raw.githubusercontent.com/autopower/thermeq3/master/install/RPi/test_relay/testrelay.py;chmod +x /home/pi/testrelay.py` 8 | 9 | # del-dev.py 10 | Simple code for deleting device from Max!Cube, please use it with caution. 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /install/alpha/readme.md: -------------------------------------------------------------------------------- 1 | # Alpha version 2 | Not so very well tested version. 3 | Just for test issues. 4 | 5 | ## Whats new? 6 | * new dahsboard, please edit dash file in cgi-bin, add `$QUERY_STRING` to the end of python line 7 | * RPi support 8 | * new config file format (json) 9 | * auto convert old config file and rename old one 10 | * send mail issues resolved 11 | * code clean up 12 | * win nt & rpi support 13 | * lifetime valve ignore 14 | * weather fixes 15 | 16 | ## How to upgrade 17 | Please [follow link](https://github.com/autopower/thermeq3/wiki/install-application#i-want-to-install-latest-working-alpha-version). 18 | -------------------------------------------------------------------------------- /obsolete/tasker/parseyun.js: -------------------------------------------------------------------------------- 1 | // JavaScript Document 2 | var arr = []; 3 | var tmp = []; 4 | var owl_title = ""; 5 | var owl_text = ""; 6 | var j = []; 7 | 8 | arr = JSON.parse(global("HTTPD")); 9 | 10 | if (arr.value.length > 2) { 11 | tmp = arr.value.slice(1, -1).split(","); 12 | } else { 13 | tmp = ""; 14 | } 15 | 16 | if (tmp.length == 0) { 17 | owl_title = "Everything OK" 18 | owl_text = "All windows closed." 19 | } else { 20 | owl_title = "Act now!" 21 | owl_text = tmp.length + " window(s) opened in room(s): " 22 | for (i=0; i < tmp.length; i++) { 23 | j = tmp[i].split(":"); 24 | owl_text += " " + j[1].slice(2, -1); 25 | } 26 | } -------------------------------------------------------------------------------- /obsolete/install/V231/config.py: -------------------------------------------------------------------------------- 1 | # Please strictly use quotation marks! 2 | # Reported name of this thermeq3 installation, ie. "thermeq3" 3 | self.devname = None 4 | # MAX Cube,ie. "192.168.0.10" 5 | self.max_ip = None 6 | # e-mail 7 | # ie. "devices@foo.local" 8 | self.fromaddr = None 9 | # ie. "user@foo.local" 10 | self.toaddr = None 11 | # SMTP host ie. "mail.foo.local" 12 | self.mailserver = None 13 | # SMTP port ie. 25 14 | self.mailport = None 15 | # SMTP authentication password, ie. "password" 16 | self.frompwd = None 17 | # Weather info 18 | # open weather map API key, ie "123456789" 19 | self.owm_api_key = None 20 | # geographic location, as per Yahoo WOEID. ie. "12345" 21 | self.location = None 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /obsolete/recreate_http.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | if [ -d /mnt/sda1 ]; then 3 | DIR=/mnt/sda1 4 | else 5 | if [ -d /mnt/sdb1 ]; then 6 | DIR=/mnt/sdb1 7 | else 8 | echo "Please mount USB or SD card!" 9 | exit 1 10 | fi 11 | fi 12 | echo "Using $DIR as storage path." 13 | echo "Recreating directories and cgi scripts" 14 | mkdir -p $DIR/www 15 | cd $DIR/www 16 | mkdir -p $DIR/csv 17 | cd $DIR/www 18 | mkdir -p cgi-bin 19 | cd cgi-bin 20 | 21 | echo "#!/bin/sh 22 | echo \"Content-type: application/json\" 23 | echo \"\" 24 | cat $DIR/www/status.xml" > status 25 | echo "#!/bin/sh 26 | echo \"Content-type: application/json\" 27 | echo \"\" 28 | cat $DIR/www/owl.xml" > owl 29 | echo "#!/bin/sh 30 | echo \"Content-type: text/html\" 31 | echo \"\" 32 | cat $DIR/www/nice.html" > nice 33 | chmod +x status 34 | chmod +x owl 35 | chmod +x nice -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Logs** 24 | If applicable, add logs to help explain your problem. 25 | 26 | **themreq3 (please complete the following information):** 27 | - OS: [e.g. OpenWRT] 28 | - Python [e.g. 2/3] 29 | - version [e.g. V298] 30 | 31 | **Hardware:** 32 | - Device: [yun/yun2/rpi/docker/linux/windows] 33 | - OS: [e.g. OpenWRT...] 34 | - Python [e.g. 2/3] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /install/RPi/readme.md: -------------------------------------------------------------------------------- 1 | # Installation on RPi 2 | 3 | ## Hardware installation 4 | Please take a look at [wiki page](https://github.com/autopower/thermeq3/wiki/Setup-RPi-hardware). 5 | ## thermeq3 installation 6 | This is very early alpha, please be very patient. 7 | Please take a look at [wiki page](https://github.com/autopower/thermeq3/wiki/install-application). 8 | 9 | ## Get config file support 10 | `wget --no-check-certificate --quiet --output-document /home/pi/thermeq3/config_me.py https://raw.githubusercontent.com/autopower/thermeq3/master/install/current/config_me.py;chmod +x /home/pi/thermeq3/config_me.py` 11 | and run it: 12 | ``` 13 | cd /home/pi/thermeq3 14 | ./config_me.py 15 | ``` 16 | Please answer questions and check final config file in JSON format vi `cat` or `vi`. 17 | 18 | ## Diagnostics 19 | Download diag.sh `wget --no-check-certificate --quiet --output-document /home/pi/thermeq3/diag.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/RPi/diag.sh; chmod +x /home/pi/thermeq3/diag.sh` 20 | -------------------------------------------------------------------------------- /eInk Display/README.md: -------------------------------------------------------------------------------- 1 | # eInk display for thermeq3 2 | Simple device with eInk display based of ESP8266 and 2.7 eInk display. 3 | This sketch using Waveshare [ESP8266 driver](https://www.waveshare.com/wiki/E-Paper_ESP8266_Driver_Board) and [2.7" display module](https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT_(B)). 4 | Please download [library](https://www.waveshare.com/wiki/File:2.7inch_e-paper_hat_b_code.7z) for 2.7" display, edit pin definitions in `epdif.h`: 5 | ``` 6 | #define CS_PIN 15 7 | #define RST_PIN 5 8 | #define DC_PIN 4 9 | #define BUSY_PIN 16 10 | ``` 11 | Finally you must edit sketch values for WiFi: 12 | ``` 13 | #define SSID_NAME "Your_SSID" 14 | #define SSID_PWD "Your_key" 15 | ``` 16 | Code using deep sleep feature on ESP, so you must connect GPIO16 and RST pin of ESP via 10k resitor, to correctly wake up. 17 | And upload sketch to ESP via Arduino IDE. 18 | 19 | thmermeq3 version must be 250+ and second website with bridge support must be installed, please check `http://ardu.ip:8180/bridge.json` if working. 20 | 21 | 22 | # Thats all folks. Stay tuned :) 23 | -------------------------------------------------------------------------------- /support/del_dev.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import base64 3 | 4 | 5 | def read_lines(): 6 | global client_socket 7 | lines_buffer = "" 8 | data = True 9 | while data: 10 | try: 11 | data = client_socket.recv(4096) 12 | lines_buffer += data 13 | except socket.timeout: 14 | break 15 | return lines_buffer 16 | 17 | 18 | print("thermeq3 delete device from MAX!Cube\n") 19 | # please edit max cube address 20 | max_ip = "192.168.0.200" 21 | # please edit device for deletion 22 | dev_id = "DEAD01" 23 | print("Deleting device id: " + dev_id + "from MAX!Cube with IP: " + max_ip) 24 | 25 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 26 | client_socket.settimeout(2) 27 | client_socket.connect((max_ip, 62910)) 28 | 29 | result = read_lines() 30 | 31 | dev_id_plain = bytearray.fromhex(dev_id).decode() 32 | message = "t:01,1," + base64.b64encode(dev_id_plain) + "\r\n" 33 | client_socket.sendall(message) 34 | 35 | # result must be "A:" 36 | print("Command issued. Please read result. Result must begin with A:\n----------") 37 | print read_lines() 38 | client_socket.close() 39 | -------------------------------------------------------------------------------- /install/dashboard/yun-dash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "thermeq3 dashboard install for yún" 3 | echo "" 4 | if [ -d /mnt/sda1 ]; then 5 | INSTALL_DIR=/mnt/sda1 6 | else 7 | if [ -d /mnt/sdb1 ]; then 8 | INSTALL_DIR=/mnt/sdb1 9 | else 10 | echo "Please mount USB or SD card!" 11 | exit 1 12 | fi 13 | fi 14 | echo "Using $INSTALL_DIR/www/cgi-bin as cgi-bin path." 15 | echo "" 16 | echo "Downloading dashboard..." 17 | wget --no-check-certificate --quiet -O $INSTALL_DIR/www/cgi-bin/dashboard.py https://github.com/autopower/thermeq3/raw/master/install/dashboard/dashboard.py 18 | if [ $? -ne 0 ]; then 19 | echo "Error during downloading dahsboard: $?" 20 | exit $? 21 | fi 22 | 23 | echo "Creating dash file" 24 | echo "#!/bin/sh 25 | /usr/bin/env python $INSTALL_DIR/www/cgi-bin/dashboard.py $QUERY_STRING" > $INSTALL_DIR/www/cgi-bin/dash 26 | chmod +x dash 27 | 28 | if ! grep -q "0.0.0.0:8180" /etc/config/uhttpd; then 29 | echo "Wrong uhttpd configuration. Please check /etc/config/uhttpd!" 30 | exit 1 31 | fi 32 | 33 | echo "Dashboard succesfully installed. Before browsing to http://arduino.ip:8180/cgi-bin/dash please edit base_dir and base_ip variables in dashboard.py file" 34 | -------------------------------------------------------------------------------- /support/testrelay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import RPi.GPIO as GPIO 3 | import time 4 | 5 | # if relay type is normally closed, set to True, normally open set to False 6 | relay_type = False 7 | 8 | 9 | def start(): 10 | GPIO.setwarnings(False) 11 | GPIO.setmode(GPIO.BCM) 12 | GPIO.setup(4, GPIO.OUT) 13 | 14 | 15 | def do(action): 16 | global relay_type 17 | if relay_type: 18 | action = not action 19 | if action: 20 | gpio_action = GPIO.HIGH 21 | else: 22 | gpio_action = GPIO.LOW 23 | try: 24 | GPIO.output(4, gpio_action) 25 | finally: 26 | pass 27 | 28 | 29 | def do_test(): 30 | global relay_type 31 | print "Using relay_type:", relay_type 32 | print "Now turning relay ON for 5sec" 33 | do(True) 34 | time.sleep(5) 35 | print "And now turn OFF" 36 | do(False) 37 | 38 | 39 | if __name__ == '__main__': 40 | print "Test relay for RPi" 41 | print "Please connect your relay control pin to PIN7 = GPIO4" 42 | start() 43 | print "Pin setup done." 44 | do_test() 45 | value = "" 46 | while not value == "y": 47 | value = raw_input("Please enter y if is this correct: ") 48 | if value == "y": 49 | pass 50 | else: 51 | relay_type = not relay_type 52 | do_test() 53 | print "You successfully setup relay. Please edit action.py, line relay_type = ", relay_type 54 | -------------------------------------------------------------------------------- /install/dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard for thermeq3 2 | Simple dashboard for boiler actor device. 3 | For complete thermeq3 readme, click [here](https://github.com/autopower/thermeq3/blob/master/README.md). 4 | 5 | To install thermeq3 *alpha* dashboard please type commands below, be sure that you are running V252+ and while logged in as root 6 | 7 | ## Installation instructions Yún 8 | * to get dashboard install script `wget --no-check-certificate --quiet -O /root/yun-dash.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/dashboard/yun-dash.sh;chmod +x /root/yun-dash.sh` 9 | * to run script `/root/yun-dash.sh` 10 | 11 | ## Installation instructions RPi 12 | * to get dashboard install script `wget --no-check-certificate --quiet -O /home/pi/rpi-dash.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/dashboard/rpi-dash.sh;chmod +x /root/rpi-dash.sh` 13 | * to run script `/home/pi/rpi-dash.sh` 14 | 15 | If script is not working, please update dashboard.py line `request = urllib2.Request("http://localhost:8180/" + str(url))` according to your setup, e.g.: 16 | * RPi users to `request = urllib2.Request("http://localhost/" + str(url))` 17 | * Yún users replace default port 8180 to port according to your uhttpd configuration `request = urllib2.Request("http://localhost:YOUR_PORT/" + str(url))` 18 | 19 | ### How to replace text in dashboard.py 20 | `sed -i -e 's/localhost:8180/localhost/g' /PATH_TO_FILE/dashboard.py` 21 | -------------------------------------------------------------------------------- /obsolete/lib/public_ip.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import socket 3 | import urllib2 4 | import os 5 | 6 | 7 | def is_private(lookup_ip): 8 | """ 9 | Returns True if IP address is private, False if not 10 | :param lookup_ip: ip address 11 | :return: True 12 | """ 13 | if os.name == "nt": 14 | f = struct.unpack('!I', socket.inet_aton(lookup_ip))[0] 15 | else: 16 | f = struct.unpack('!I', socket.inet_pton(socket.AF_INET, lookup_ip))[0] 17 | private = ( 18 | [2130706432, 4278190080], # 127.0.0.0, 255.0.0.0 http://tools.ietf.org/html/rfc3330 19 | [3232235520, 4294901760], # 192.168.0.0, 255.255.0.0 http://tools.ietf.org/html/rfc1918 20 | [2886729728, 4293918720], # 172.16.0.0, 255.240.0.0 http://tools.ietf.org/html/rfc1918 21 | [167772160, 4278190080], # 10.0.0.0, 255.0.0.0 http://tools.ietf.org/html/rfc1918 22 | ) 23 | for net in private: 24 | if f & net[1] == net[0]: 25 | return True 26 | return False 27 | 28 | 29 | def get(): 30 | """ 31 | Gets public IP, using service at ip.42.pl/raw address 32 | :return: ip address, if unsuccessful 0xFF 33 | """ 34 | try: 35 | tmp_ip = str(urllib2.urlopen('http://ip.42.pl/raw').read()) 36 | except Exception: 37 | try: 38 | tmp_ip = socket.gethostbyname(socket.gethostname()) 39 | except Exception: 40 | tmp_ip = 0xFF 41 | return tmp_ip 42 | -------------------------------------------------------------------------------- /install/RPi/diag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ $# -lt 1 ]; then 3 | echo "Usage: diag.sh " 4 | echo "diag.sh 192.168.0.222" 5 | exit 1 6 | fi 7 | 8 | DIAGFILE=/home/pi/thermeq3/diag.txt 9 | 10 | PKG_OK=$(dpkg-query -W --showformat='${Status}\n' telnet|grep "install ok installed") 11 | echo Checking for telnet: $PKG_OK 12 | if [ "" == "$PKG_OK" ]; then 13 | echo "No telnet. Installing telnet" 14 | sudo apt-get --yes install telnet 15 | fi 16 | 17 | if [ -f /home/pi/thermeq3/diag.txt ]; then 18 | echo "Previous diag.txt exists! Appending to file." 19 | fi 20 | echo "--- Diag script ---------" >> $DIAGFILE 21 | date >> $DIAGFILE 22 | echo "df..." 23 | echo "--- Diskfree ---------" >> $DIAGFILE 24 | df -h >> $DIAGFILE 25 | echo "mount..." 26 | echo "--- Mount point ---------" >> $DIAGFILE 27 | mount >> $DIAGFILE 28 | echo "ifconfig..." 29 | echo "--- i/f config ---------" >> $DIAGFILE 30 | ifconfig >> $DIAGFILE 31 | echo "ping..." 32 | echo "--- Ping ---------" >> $DIAGFILE 33 | ping $1 -c 4 >> $DIAGFILE 34 | echo "telnet..." 35 | echo "--- Telnet to MAX!Cube ---------" >> $DIAGFILE 36 | sleep 3 | telnet $1 62910 >> $DIAGFILE 37 | read -p "Include thermeq3.json file? WARNING: file may contains user credentials to email service! [y/n]" yn 38 | case $yn in 39 | [Yy]* ) 40 | echo "--- thermeq3.json ---------" >> $DIAGFILE 41 | cat /home/pi/thermeq3/thermeq3.json >> $DIAGFILE 42 | ;; 43 | esac 44 | echo "Please post diag.txt to autopowerdevice@gmail.com" -------------------------------------------------------------------------------- /obsolete/tasker/parseyun-tasker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1338504847144 4 | 1419613034489 5 | 14 6 | Get thermeq3 Status 7 | 10 8 | 9 | 118 10 | http://user:password@thermeq.ip/data/get/openwinlist 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 37 22 | 23 | 24 | %HTTPR 25 | 0 26 | 200 27 | 28 | 29 | 30 | 31 | 131 32 | Tasker/js/parseyun.js 33 | 34 | 35 | 36 | 37 | 38 | 37 39 | 40 | 41 | %owl_title 42 | 3 43 | Everything OK 44 | 45 | 46 | 47 | 48 | 550 49 | %owl_title 50 | %owl_text 51 | 52 | Popup 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /obsolete/lib/dummy.py: -------------------------------------------------------------------------------- 1 | import thermeq3 2 | import datetime 3 | 4 | 5 | def add_dummy(status): 6 | """ 7 | :param status: is window open? 8 | :return: nothing 9 | """ 10 | # valves = {valve_adr: [valve_pos, valve_temp, valve_curtemp, valve_name]} 11 | # rooms = {id : [room_name, room_address, is_win_open, curr_temp, average valve position]} 12 | # devices = {addr: [type, serial, name, room, OW, OW_time, status, info, temp offset]} 13 | thermeq3.t3.eq3.rooms.update({"99": ["Dummy room", "DeadBeefValve", False, 22.0, 22]}) 14 | thermeq3.t3.eq3.devices.update({"DeadBeefWindow": [4, "IHADBW", "Dummy window", 99, 0, 15 | datetime.datetime(2016, 01, 01, 12, 00, 00), 18, 16, 7]}) 16 | thermeq3.t3.eq3.devices.update({"DeadBeefValve": [1, "IHADBV", "Dummy valve", 99, 0, 17 | datetime.datetime(2016, 01, 01, 12, 00, 00), 18, 56, 7]}) 18 | thermeq3.t3.eq3.valves.update({"DeadBeefValve": [20, 22.0, 22.0, "Dummy valve"]}) 19 | # TBI open/closed window 20 | if status: 21 | thermeq3.t3.eq3.devices["DeadBeefWindow"][4] = 2 22 | thermeq3.t3.eq3.devices["DeadBeefWindow"][5] = \ 23 | datetime.datetime.now() - \ 24 | datetime.timedelta(seconds=((thermeq3.t3.eq3.ignore_time + 10) * 60)) 25 | thermeq3.t3.eq3.rooms["99"][2] = True 26 | else: 27 | thermeq3.t3.eq3.devices["DeadBeefWindow"][4] = 0 28 | thermeq3.t3.eq3.rooms["99"][2] = False 29 | 30 | 31 | def remove_dummy(): 32 | del thermeq3.t3.eq3.rooms["99"] 33 | del thermeq3.t3.eq3.valves["DeadBeefValve"] 34 | del thermeq3.t3.eq3.devices["DeadBeefWindow"] 35 | del thermeq3.t3.eq3.devices["DeadBeefValve"] 36 | -------------------------------------------------------------------------------- /install/RPi/install-dash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | WWWDIR=/var/www/html 3 | CGIDIR=/usr/lib/cgi-bin 4 | BASE_DIR=/home/pi/thermeq3 5 | 6 | echo "Using $WWWDIR as base for html files" 7 | echo "Using $CGIDIR as cgi-bin path" 8 | 9 | PKG_OK=$(dpkg-query -W --showformat='${Status}\n' apache2|grep "install ok installed") 10 | echo "Checking for apache2: $PKG_OK" 11 | if [ "" == "$PKG_OK" ]; then 12 | echo "No apache2. Installing apache2" 13 | sudo apt-get --force-yes --yes install apache2 14 | if [ $? -ne 0 ]; then 15 | echo "Error during installing apache2 package. Error: $?" 16 | exit $? 17 | fi 18 | fi 19 | 20 | echo "Creating apache configuration..." 21 | sudo chown -R pi:www-data /var/www/html/ 22 | sudo chmod -R 770 /var/www/html/ 23 | 24 | echo "Downloading bootstrap" 25 | wget --no-check-certificate --quiet --output-document $WWWDIR/bootstrap.zip https://github.com/twbs/bootstrap/releases/download/v3.3.7/bootstrap-3.3.7-dist.zip 26 | echo "Unzipping bootstrap" 27 | unzip $WWWDIR/bootstrap.zip -d $WWWDIR/ 28 | if [ $? -ne 0 ]; then 29 | echo "Error during unzipping thermeq3 app: $?" 30 | exit $? 31 | fi 32 | echo "Moving bootstrap..." 33 | cp -R $WWWDIR/bootstrap-3.3.7-dist/* $WWWDIR/ 34 | rm -rf $WWWDIR/bootstrap-3.3.7-dist/ 35 | rm -rf $WWWDIR/bootstrap.zip 36 | 37 | echo "Downloading dashboard..." 38 | sudo wget --no-check-certificate --quiet --output-document $CGIDIR/dashboard.py https://github.com/autopower/thermeq3/raw/master/install/dashboard/dashboard.py 39 | if [ $? -ne 0 ]; then 40 | echo "Error during downloading dashboard: $?" 41 | exit $? 42 | fi 43 | 44 | echo "Creating dash file" 45 | echo "#!/bin/sh 46 | /usr/bin/env python $CGIDIR/dashboard.py" > ~/dash 47 | sudo chmod +x ~/dash 48 | sudo mv ~/dash $CGIDIR/dash 49 | 50 | echo "Dashboard succesfully installed. Before browsing to http://rpi.ip/cgi-bin/dash please edit credentials in dashboard.py file" 51 | -------------------------------------------------------------------------------- /install/dashboard/rpi-dash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "thermeq3 dashboard install for Rpi" 3 | echo "" 4 | WWWDIR=/var/www/html 5 | CGIDIR=/usr/lib/cgi-bin 6 | BASE_DIR=/home/pi/thermeq3 7 | 8 | echo "Using $WWWDIR as base for html files" 9 | echo "Using $CGIDIR as cgi-bin path" 10 | 11 | PKG_OK=$(dpkg-query -W --showformat='${Status}\n' apache2|grep "install ok installed") 12 | echo "Checking for apache2: $PKG_OK" 13 | if [ "" == "$PKG_OK" ]; then 14 | echo "No apache2. Installing apache2" 15 | sudo apt-get --force-yes --yes install apache2 16 | if [ $? -ne 0 ]; then 17 | echo "Error during installing apache2 package. Error: $?" 18 | exit $? 19 | fi 20 | fi 21 | 22 | echo "Creating apache configuration..." 23 | sudo chown -R pi:www-data /var/www/html/ 24 | sudo chmod -R 770 /var/www/html/ 25 | sudo a2enmod cgi 26 | sudo service apache2 restart 27 | 28 | echo "" 29 | echo "Downloading dashboard..." 30 | sudo wget --no-check-certificate --quiet --output-document $CGIDIR/dashboard.py https://github.com/autopower/thermeq3/raw/master/install/dashboard/dashboard.py 31 | if [ $? -ne 0 ]; then 32 | echo "Error during downloading dashboard: $?" 33 | exit $? 34 | fi 35 | 36 | echo "Creating dash file" 37 | echo "#!/bin/sh 38 | /usr/bin/env python $CGIDIR/dashboard.py" > ~/dash 39 | echo " - chmod +x" 40 | sudo chmod +x ~/dash 41 | echo " - move file" 42 | sudo mv ~/dash $CGIDIR/dash.sh 43 | echo " - replace ip:port string" 44 | sudo sed -i -e 's/localhost:8180/localhost/g' /usr/lib/cgi-bin/dashboard.py 45 | 46 | echo "Modyfying apache config files" 47 | echo "" 48 | if ! grep -Fq 'AddHandler cgi-script .sh' /etc/apache2/conf-available/serve-cgi-bin.conf ; then 49 | echo " - add .sh handler in cgi-bin" 50 | sudo sed -i '//a AddHandler cgi-script .sh' /etc/apache2/conf-available/serve-cgi-bin.conf 51 | fi 52 | if ! grep -Fq '#Include conf-available\/serve-cgi-bin.conf' /etc/apache2/sites-available/000-default.conf ; then 53 | echo " - include serve cgi-bin config file" 54 | sudo sed -i 's/#Include conf-available\/serve-cgi-bin.conf/Include conf-available\/serve-cgi-bin.conf/g' /etc/apache2/sites-available/000-default.conf 55 | fi 56 | echo "Dashboard succesfully installed." 57 | -------------------------------------------------------------------------------- /obsolete/lib/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | def load_old(old_config_file, new_config_file): 6 | cf_1 = ["self.devname", "self.max_ip", "self.fromaddr", "self.toaddr", "self.mailserver", "self.mailport", 7 | "self.frompwd", "self.owm_api_key", "self.location", "self.extport"] 8 | cf_2 = ["self.device_name", "self.max_ip", "self.from_addr", "self.to_addr", "self.mail_server", "self.mail_port", 9 | "self.from_pwd", "self.owm_api_key", "self.yahoo_location", "self.ext_port"] 10 | ncf = {} 11 | result = 0xFF 12 | if os.path.exists(old_config_file): 13 | try: 14 | f = open(old_config_file, "r") 15 | except IOError: 16 | result = 1 17 | else: 18 | num = 0 19 | for line in f: 20 | is_comment = line.find('#') 21 | if is_comment == 0: 22 | pass 23 | else: 24 | if is_comment > 0: 25 | line = line[:-is_comment] 26 | w = line.split("=") 27 | wr = w[0].rstrip() 28 | if not w[0] == "\n": 29 | if "toaddr" in wr: 30 | wv = w[1].lstrip().rstrip() 31 | else: 32 | wv = w[1].lstrip().rstrip().replace('"', '') 33 | if wr in cf_1: 34 | num += 1 35 | idx = cf_1.index(wr) 36 | ncf.update({cf_2[idx].replace("self.", ""): str(wv)}) 37 | if num < len(cf_1): 38 | # some commands are missing 39 | result = 2 40 | else: 41 | # everything is ok 42 | result = 3 43 | f.close() 44 | try: 45 | f = open(new_config_file, "w") 46 | except IOError: 47 | result = 4 48 | else: 49 | json.dump(ncf, f) 50 | f.close() 51 | result = 0 52 | return result 53 | 54 | 55 | def load(config_file): 56 | f = None 57 | result = {} 58 | try: 59 | f = open(config_file, "r") 60 | except IOError: 61 | pass 62 | else: 63 | if f is not None: 64 | result = json.load(f) 65 | finally: 66 | if f is not None: 67 | f.close() 68 | return result 69 | -------------------------------------------------------------------------------- /obsolete/lib/secweb.py: -------------------------------------------------------------------------------- 1 | import logmsg 2 | import os 3 | import errno 4 | 5 | 6 | sw = None 7 | 8 | 9 | class SecWeb(object): 10 | def __init__(self): 11 | self.location = {} 12 | 13 | 14 | # 15 | # Global module functions 16 | # 17 | def init(location): 18 | """ 19 | Setup variables 20 | :param location: where is secondary web directory 21 | :return: nothing 22 | """ 23 | global sw 24 | sw = SecWeb() 25 | sw.location = { 26 | "status": str(location + "www/status.json"), 27 | "owl": str(location + "www/owl.json"), 28 | "nice": str(location + "www/nice.html"), 29 | "bridge": str(location + "www/bridge.json"), 30 | "loc": str(location + "www/location.json")} 31 | # check if exists directory for secondary web, if needed, create 32 | try: 33 | os.makedirs(location + "www") 34 | except OSError as exception: 35 | if exception.errno != errno.EEXIST: 36 | raise 37 | 38 | 39 | def write(cw, message): 40 | """ 41 | Save message to file which is in secondary web directory 42 | :param message: message or string to write to file 43 | :param cw: string, selector what to save 44 | """ 45 | global sw 46 | if cw in sw.location: 47 | fn = sw.location[str(cw)] 48 | try: 49 | swf = open(fn, "w") 50 | except IOError as e: 51 | logmsg.update("Error opening file " + fn + "! I/O error({0}): {1}".format(e.errno, e.strerror)) 52 | except EnvironmentError: 53 | logmsg.update("Environment error while opening " + fn + "!", 'E') 54 | else: 55 | try: 56 | swf.write(str(message)) 57 | except EnvironmentError: 58 | logmsg.update("Environment error while writing " + fn + "!", 'E') 59 | swf.close() 60 | else: 61 | logmsg.update("Wrong target [" + str(cw) + "] for saving file!", 'E') 62 | 63 | 64 | def nice(message): 65 | message.replace("\n", "
") 66 | message.replace("\t", " ") 67 | write("nice", "\n\nStatus\n\n

" + message + "

\n\n") 68 | 69 | 70 | def save_location(location): 71 | global sw 72 | try: 73 | f = open(sw.location["loc"], "w") 74 | except IOError: 75 | logmsg.update("IOError during opening location file for writing!", 'E') 76 | f.write(str({"location":str(location)})) 77 | f.close() 78 | -------------------------------------------------------------------------------- /obsolete/lib/support.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import os 3 | import datetime 4 | import urllib2 5 | 6 | 7 | run_target = "" 8 | 9 | 10 | def guess_platform(): 11 | """ 12 | :return: Boolean 13 | """ 14 | global run_target 15 | gos = str(platform.platform()).upper() 16 | target = "rpi" 17 | if "WINDOWS" in gos: 18 | target = "win" 19 | elif "LINUX" in gos: 20 | gm = str(platform.machine()).upper() 21 | if "MIPS" in gm: 22 | target = "yun" 23 | elif "ARM" in gm: 24 | target = "rpi" 25 | run_target = target 26 | 27 | 28 | def is_yun(): 29 | """ 30 | Return True if platform is yun 31 | :return: Boolean 32 | """ 33 | global run_target 34 | if run_target == "yun": 35 | return True 36 | else: 37 | return False 38 | 39 | 40 | def is_rpi(): 41 | """ 42 | Return True if platform is rpi 43 | :return: Boolean 44 | """ 45 | global run_target 46 | if run_target == "rpi": 47 | return True 48 | else: 49 | return False 50 | 51 | 52 | def is_win(): 53 | """ 54 | Return True if platform is win 55 | :return: Boolean 56 | """ 57 | global run_target 58 | if run_target == "win": 59 | return True 60 | else: 61 | return False 62 | 63 | 64 | def get_uptime(): 65 | if os.name != "nt": 66 | with open("/proc/uptime", "r") as f: 67 | uptime_seconds = float(f.readline().split()[0]) 68 | return_str = str(datetime.timedelta(seconds=uptime_seconds)).split(".")[0] 69 | else: 70 | uptime = os.popen('systeminfo', 'r') 71 | # obfuscate warning 72 | data = uptime.readlines() 73 | data += "" 74 | uptime.close() 75 | return_str = str(0) 76 | return return_str 77 | 78 | 79 | def is_empty(var): 80 | if var == "" or var is None or var == "None": 81 | return True 82 | else: 83 | return False 84 | 85 | 86 | def io_error(err): 87 | """ 88 | :param err: Exception handler 89 | :return: string 90 | """ 91 | return "I/O error({0}): {1}".format(err.errno, err.strerror) 92 | 93 | 94 | def call_home(selector): 95 | if selector == "apprun": 96 | url = "https://www.google-analytics.com/collect?v=1&t=event&tid=UA-106611241-1&cid=1&ec=App&ea=Run" 97 | elif selector == "applive": 98 | url = "https://www.google-analytics.com/collect?v=1&t=event&tid=UA-106611241-1&cid=1&ec=App&ea=Live" 99 | else: 100 | url = "https://www.google-analytics.com/collect?v=1&t=event&tid=UA-106611241-1&cid=1&ec=App&ea=Other" 101 | try: 102 | urllib2.urlopen(url).read() 103 | except Exception: 104 | pass 105 | -------------------------------------------------------------------------------- /obsolete/lib/logmsg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import logging.handlers 4 | import inspect 5 | 6 | log_messages = [] 7 | logs = ['I', 'D', 'E'] 8 | logger = None 9 | 10 | 11 | def update(message, log='D'): 12 | """ 13 | Update list with message and log level 14 | :param message: string 15 | :param log: char, log selector 16 | :return: 17 | """ 18 | global log_messages, logs 19 | if log in logs: 20 | log_messages.append([log, message]) 21 | else: 22 | log_messages.append(['E', message]) 23 | flush() 24 | 25 | 26 | def debug(*args): 27 | """ 28 | Converts arguments to strings, concat them and update log 29 | :param args: argument list 30 | :return: nothing 31 | """ 32 | ln = len(args) 33 | if ln > 0: 34 | tmp_str = "## " + str(inspect.stack()[1][3]) + " ## " 35 | for i in range(0, ln): 36 | tmp_str += str(args[i]) 37 | update(tmp_str, 'D') 38 | 39 | 40 | def flush(): 41 | """ 42 | Flush log messages 43 | :return: nothing 44 | """ 45 | global log_messages, logger 46 | # if list is not empty 47 | if log_messages: 48 | for k in log_messages: 49 | if k[0] == 'E': 50 | logger.error(k[1]) 51 | elif k[0] == 'D': 52 | logger.debug(k[1]) 53 | elif k[0] == 'I': 54 | logger.info(k[1]) 55 | elif k[0] == 'C': 56 | logger.critical(k[1]) 57 | else: 58 | logger.error("Wrong level: " + str(k[0]) + " message: " + str(k[1])) 59 | log_messages = [] 60 | 61 | 62 | def start(log_filename): 63 | """ 64 | Start writing to time rotated log file 65 | :param log_filename: string 66 | :return: 67 | """ 68 | global logger 69 | logger = logging.getLogger("thermeq3") 70 | logger.setLevel(logging.DEBUG) 71 | 72 | try: 73 | fh = logging.handlers.TimedRotatingFileHandler(log_filename, when="W0", interval=4, backupCount=12) 74 | except Exception: 75 | print log_filename 76 | raise 77 | fh.setLevel(logging.DEBUG) 78 | 79 | formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y/%m/%d %H:%M:%S") 80 | fh.setFormatter(formatter) 81 | logger.addHandler(fh) 82 | 83 | logger.info("Started with PID=" + str(os.getpid())) 84 | 85 | 86 | def level(selector): 87 | """ 88 | Changes log level 89 | :param selector: char 90 | :return: nothing 91 | """ 92 | global logger 93 | if selector == 'D': 94 | logger.setLevel(logging.DEBUG) 95 | elif selector == 'I': 96 | logger.setLevel(logging.INFO) 97 | 98 | 99 | def stop(): 100 | """ 101 | Stops logging 102 | :return: nothing 103 | """ 104 | global logger 105 | logger.close() 106 | -------------------------------------------------------------------------------- /obsolete/install/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | if [ $# -lt 1 ]; then 3 | echo "Usage: install.sh " 4 | exit 1 5 | fi 6 | 7 | echo "Preparing..." 8 | echo "opkg update" 9 | opkg update --verbosity=0 10 | if [ $? -ne 0 ]; then 11 | echo "Error during opkg update: $?" 12 | exit $? 13 | fi 14 | 15 | mkdir /root/thermeq3-install 16 | echo Downloading thermeq3 app 17 | wget --no-check-certificate --quiet --output-document /root/nsm.py https://github.com/autopower/thermeq3/raw/master/install/nsm.py 18 | if [ $? -ne 0 ]; then 19 | echo "Error during downloading thermeq3 app: $?" 20 | exit $? 21 | fi 22 | 23 | echo Downloading thermeq3 config file 24 | wget --no-check-certificate --quiet --output-document /root/config.py https://github.com/autopower/thermeq3/raw/master/install/config.py 25 | if [ $? -ne 0 ]; then 26 | echo "Error during downloading thermeq3 config file: $?" 27 | exit $? 28 | fi 29 | 30 | echo "Installing libraries" 31 | opkg install python-openssl --verbosity=0 32 | if [ $? -ne 0 ]; then 33 | echo "Error during installing openssl library. Error: $?" 34 | exit $? 35 | fi 36 | opkg install python-expat --verbosity=0 37 | if [ $? -ne 0 ]; then 38 | echo "Error during installing expat library. Error: $?" 39 | exit $? 40 | fi 41 | 42 | if [ -d /mnt/sda1 ]; then 43 | DIR=/mnt/sda1 44 | else 45 | if [ -d /mnt/sdb1 ]; then 46 | DIR=/mnt/sdb1 47 | else 48 | echo "Please mount USB or SD card!" 49 | exit 1 50 | fi 51 | fi 52 | echo "Using $DIR as storage path." 53 | 54 | if ! grep -q "0.0.0.0:8180" /etc/config/uhttpd; then 55 | echo "Backing up uhttpd configuration..." 56 | mkdir /root/backup 57 | cp /etc/config/uhttpd /root/backup/uhttpd.old 58 | echo "Modifying uhttpd configuration..." 59 | echo "config uhttpd secondary 60 | list listen_http 0.0.0.0:8180 61 | option home $DIR/www 62 | option cgi_prefix /cgi-bin 63 | option max_requests 2 64 | option script_timeout 10 65 | option network_timeout 10 66 | option tcp_keepalive 1 67 | " >> /etc/config/uhttpd 68 | echo "Restarting uhttpd..." 69 | /etc/init.d/uhttpd restart 70 | fi 71 | 72 | echo "Creating directories and cgi scripts" 73 | mkdir -p $DIR/www 74 | cd $DIR/www 75 | mkdir -p cgi-bin 76 | cd cgi-bin 77 | 78 | echo "#!/bin/sh 79 | echo \"Content-type: application/json\" 80 | echo \"\" 81 | cat $DIR/www/status.xml" > status 82 | echo "#!/bin/sh 83 | echo \"Content-type: application/json\" 84 | echo \"\" 85 | cat $DIR/www/owl.xml" > owl 86 | echo "#!/bin/sh 87 | echo \"Content-type: text/html\" 88 | echo \"\" 89 | cat $DIR/www/nice.html" > nice 90 | chmod +x status 91 | chmod +x owl 92 | chmod +x nice 93 | 94 | echo "Installing scripts with $1 as device name and $DIR as target directory" 95 | echo "tail -n 50 $DIR/$1.log" > /root/ct 96 | echo "cat $DIR/$1_error.log" > /root/err 97 | echo "ps|grep python" > /root/psg 98 | chmod +x /root/ct 99 | chmod +x /root/err 100 | chmod +x /root/psg 101 | echo "Please edit config.py file!" -------------------------------------------------------------------------------- /obsolete/install/README.md: -------------------------------------------------------------------------------- 1 | # thermeq3 2 | ### Install application 3 | Access yún via ssh (e.g. Windows users can use putty) 4 | * Update `opkg`: `opkg update` 5 | * Update `wget`: `opkg upgrade wget` 6 | * Install `thermeq3`, please look below for help 7 | * Run the installer script: `/root/install.sh ` (if you are upgrading from V231-, run `/root/upgrade.sh`), for example `/root/install.sh boilerstarter`, this `boilerstarter` name will be used as 8 | device name 9 | * **Edit required values in the config file** Please take a look below how to modify config file. 10 | * You'll need SMTP server details and [Open Weather Map API key](http://openweathermap.org/appid) (sign-up is free). 11 | 12 | ### I want to install version below 231 13 | For v200+, use `wget --no-check-certificate --quiet --output-document /root/install.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/current/install.sh;chmod +x /root/upgrade.sh` 14 | 15 | ### I want to install v150 16 | Versions below 200 are obsolete! For v150, use `wget --no-check-certificate --quiet --output-document /root/install.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/obsolete/install.sh;chmod +x /root/upgrade.sh` 17 | 18 | ### Upgrading from V2xx to V231+ 19 | **If you are upgrading from version below V231** and you have working installation, please use [this script](https://github.com/autopower/thermeq3/tree/master/install/current/upgrade.sh) or `wget --no-check-certificate --quiet --output-document /root/upgrade.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/current/upgrade.sh;chmod +x /root/upgrade.sh`. 20 | 21 | ### Modifying config file 22 | #### V199- 23 | You can edit `config.py` file with default editor `vi`. If you are no familiar with `vi` (try [this man](https://www.freebsd.org/cgi/man.cgi?vi)), you can use your favourite editor on your platform and transfer file via ftp/scp. For example if you are using windows, you can use [pspad](http://www.pspad.com/en/) and transfer file via [winscp](https://winscp.net/eng/index.php). 24 | * `stp.max_ip = "192.168.0.10"` IP address of MAX! cube 25 | * `stp.fromaddr = "devices@foo.local"` from, user name 26 | * `stp.toaddr = "user@foo.local"` to email 27 | * `stp.mailserver = "mail.foo.local"` via this server 28 | * `stp.mailport = 25` on this port 29 | * `stp.frompwd = "this.is.password"` login with this password 30 | * `stp.devname = "hellmostat"` device name 31 | * `stp.timeout = 10` timeout in secods, used in communicating with MAX! Cube and as a sleep time for flushing msg queue, set to similar value as `unsigned long interval` in arduino sketch 32 | * `stp.extport = 29080` external port, this is the port (typically) on firewall where NAT is defined (so you can mute thermeq3 from internet), please setup your firewall/router to such scenario 33 | * `stp.owm_api_key = "your owm api key"` this is API key for OWM service 34 | 35 | #### V200 to V230 36 | For V200+ `stp.` is replaced with `self.setup.` or `self.` so `stp.max_ip` becomes `self.setup.max_ip`. 37 | 38 | #### V231 to V249 39 | **Version 231+ automatically reads old config.py file format (plain python code) and converts it to JSON format.** 40 | If you are using V231+ please use current config file in JSON format with name `thermeq.json` 41 | 42 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at autopowerdevice@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /obsolete/install/V231/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | if [ $# -lt 1 ]; then 3 | echo "Usage: install.sh " 4 | exit 1 5 | fi 6 | 7 | echo "Preparing..." 8 | echo "opkg update" 9 | opkg update --verbosity=0 10 | if [ $? -ne 0 ]; then 11 | echo "Error during opkg update: $?" 12 | exit $? 13 | fi 14 | 15 | echo "Installing zip" 16 | opkg install unzip --verbosity=0 17 | if [ $? -ne 0 ]; then 18 | echo "Error during installing unzip. Error: $?" 19 | exit $? 20 | fi 21 | 22 | mkdir /root/thermeq3-install 23 | echo Downloading thermeq3 app 24 | wget --no-check-certificate --quiet --output-document /root/thermeq3-install/thermeq3.zip https://github.com/autopower/thermeq3/raw/master/install/current/thermeq3.zip 25 | if [ $? -ne 0 ]; then 26 | echo "Error during downloading thermeq3 app: $?" 27 | exit $? 28 | fi 29 | mkdir /root/thermeq3 30 | echo "Unzipping app" 31 | unzip -q /root/thermeq3-install/thermeq3.zip -d /root/thermeq3 32 | if [ $? -ne 0 ]; then 33 | echo "Error during unzipping thermeq3 app: $?" 34 | exit $? 35 | fi 36 | 37 | echo Downloading thermeq3 config file 38 | wget --no-check-certificate --quiet --output-document /root/config.py https://github.com/autopower/thermeq3/raw/master/install/current/config.py 39 | if [ $? -ne 0 ]; then 40 | echo "Error during downloading thermeq3 config file: $?" 41 | exit $? 42 | fi 43 | 44 | echo "Installing libraries" 45 | opkg install python-openssl --verbosity=0 46 | if [ $? -ne 0 ]; then 47 | echo "Error during installing openssl library. Error: $?" 48 | exit $? 49 | fi 50 | opkg install python-expat --verbosity=0 51 | if [ $? -ne 0 ]; then 52 | echo "Error during installing expat library. Error: $?" 53 | exit $? 54 | fi 55 | 56 | if [ -d /mnt/sda1 ]; then 57 | DIR=/mnt/sda1 58 | else 59 | if [ -d /mnt/sdb1 ]; then 60 | DIR=/mnt/sdb1 61 | else 62 | echo "Please mount USB or SD card!" 63 | exit 1 64 | fi 65 | fi 66 | echo "Using $DIR as storage path." 67 | 68 | if ! grep -q "0.0.0.0:8180" /etc/config/uhttpd; then 69 | echo "Backing up uhttpd configuration..." 70 | mkdir /root/backup 71 | cp /etc/config/uhttpd /root/backup/uhttpd.old 72 | echo "Modifying uhttpd configuration..." 73 | echo "config uhttpd secondary 74 | list listen_http 0.0.0.0:8180 75 | option home $DIR/www 76 | option cgi_prefix /cgi-bin 77 | option max_requests 2 78 | option script_timeout 10 79 | option network_timeout 10 80 | option tcp_keepalive 1 81 | " >> /etc/config/uhttpd 82 | echo "Restarting uhttpd..." 83 | /etc/init.d/uhttpd restart 84 | fi 85 | 86 | echo "Creating directories and cgi scripts" 87 | mkdir -p $DIR/www 88 | cd $DIR/www 89 | mkdir -p $DIR/csv 90 | cd $DIR/www 91 | mkdir -p cgi-bin 92 | cd cgi-bin 93 | 94 | echo "#!/bin/sh 95 | echo \"Content-type: application/json\" 96 | echo \"\" 97 | cat $DIR/www/status.xml" > status 98 | echo "#!/bin/sh 99 | echo \"Content-type: application/json\" 100 | echo \"\" 101 | cat $DIR/www/owl.xml" > owl 102 | echo "#!/bin/sh 103 | echo \"Content-type: text/html\" 104 | echo \"\" 105 | cat $DIR/www/nice.html" > nice 106 | chmod +x status 107 | chmod +x owl 108 | chmod +x nice 109 | 110 | echo "Creating nsm.py compatibility file" 111 | echo "#!/usr/bin/env python 112 | import sys 113 | 114 | sys.path.insert(0, \"/root/thermeq3/\") 115 | execfile(\"/root/thermeq3/nsm.py\") 116 | " > /root/nsm.py 117 | 118 | echo "Installing scripts with $1 as device name and $DIR as target directory" 119 | echo "tail -n 50 $DIR/$1.log" > /root/ct 120 | echo "cat $DIR/$1_error.log" > /root/err 121 | echo "ps|grep python" > /root/psg 122 | chmod +x /root/ct 123 | chmod +x /root/err 124 | chmod +x /root/psg 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /obsolete/lib/csvfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import logmsg 4 | 5 | 6 | class CsvObject(object): 7 | def __init__(self): 8 | self.file = "" 9 | self.handle = None 10 | self.state = False 11 | self.place = "" 12 | self.dev_name = "" 13 | self._buffer = "" 14 | 15 | def __repr__(self): 16 | return str(self.__class__) + ": " + str(self.__dict__) 17 | 18 | def __str__(self): 19 | return str(self.__class__) + ": " + str(self.__dict__) 20 | 21 | def is_init(self): 22 | return self.file == "" and self.place == "" and self.dev_name == "" 23 | 24 | def open(self): 25 | if self.is_init(): 26 | raise NameError("Wrong init!") 27 | if os.path.exists(self.file): 28 | try: 29 | os.rename(self.file, 30 | self.place + self.dev_name + "_" + time.strftime("%Y%m%d-%H%M%S", time.localtime()) + ".csv") 31 | except Exception: 32 | pass 33 | try: 34 | self.handle = open(self.file, "a") 35 | except IOError: 36 | this_path = os.path.dirname(self.file) 37 | if not os.path.exists(this_path): 38 | try: 39 | os.makedirs(this_path) 40 | except Exception: 41 | logmsg.update("Can't create directory for CSV files (" + str(this_path) + ")") 42 | raise 43 | except Exception: 44 | logmsg.update("Can't open CSV file: " + str(self.file)) 45 | raise 46 | else: 47 | self.state = True 48 | 49 | def close(self): 50 | try: 51 | self.handle.flush() 52 | except: 53 | pass 54 | 55 | try: 56 | os.fsync(self.handle.fileno()) 57 | except: 58 | pass 59 | 60 | try: 61 | self.handle.close() 62 | except Exception: 63 | logmsg.update("Can't close CSV file!") 64 | else: 65 | self.state = False 66 | 67 | def add(self, text): 68 | self._buffer += text + "," 69 | 70 | def write(self): 71 | if self._buffer[-1:] == ',': 72 | self._buffer = self._buffer[:-1] 73 | self.handle.write(self._buffer + "\n") 74 | self.handle.flush() 75 | self._buffer = "" 76 | 77 | 78 | csv = CsvObject() 79 | 80 | 81 | def is_init(): 82 | """ 83 | Is CSV initalized? 84 | :return: boolean 85 | """ 86 | global csv 87 | return csv.state 88 | 89 | 90 | def init(place, dev_name): 91 | """ 92 | Init CSV variables and open CSV file 93 | :param place: 94 | :param dev_name: 95 | :return: boolean 96 | """ 97 | global csv 98 | if not place == "": 99 | if place[-1:] != '/': 100 | place += '/' 101 | place += "csv/" 102 | csv.place = place 103 | else: 104 | return False 105 | if not dev_name == "": 106 | csv.dev_name = dev_name 107 | else: 108 | return False 109 | csv.file = place + dev_name + ".csv" 110 | csv.state = True 111 | csv.open() 112 | 113 | 114 | def close(): 115 | global csv 116 | """ 117 | Close CSV file 118 | :return: nothing 119 | """ 120 | csv.close() 121 | 122 | 123 | def write(*args): 124 | """ 125 | Write arguments to CSV file 126 | :param args: arguments 127 | :return: nothing 128 | """ 129 | global csv 130 | if not is_init: 131 | csv.open() 132 | ln = len(args) 133 | if ln > 0: 134 | for i in range(0, ln): 135 | if args[i] == "\n": 136 | csv.write() 137 | else: 138 | csv.add(str(args[i])) 139 | -------------------------------------------------------------------------------- /obsolete/install/alpha_Yun_Rev2/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | if [ $# -lt 1 ]; then 3 | echo "Usage: install.sh " 4 | exit 1 5 | fi 6 | 7 | echo "Preparing..." 8 | echo "opkg update" 9 | opkg update --verbosity=0 10 | if [ $? -ne 0 ]; then 11 | echo "Error during opkg update: $?" 12 | exit $? 13 | fi 14 | 15 | echo "Installing zip" 16 | opkg install unzip --verbosity=0 17 | if [ $? -ne 0 ]; then 18 | echo "Error during installing unzip. Error: $?" 19 | exit $? 20 | fi 21 | 22 | mkdir /root/thermeq3-install 23 | echo Downloading thermeq3 app 24 | wget --no-check-certificate --quiet -O /root/thermeq3-install/thermeq3.zip https://github.com/autopower/thermeq3/raw/master/install/current/thermeq3.zip 25 | if [ $? -ne 0 ]; then 26 | echo "Error during downloading thermeq3 app: $?" 27 | exit $? 28 | fi 29 | mkdir /root/thermeq3 30 | echo "Unzipping app" 31 | unzip -q /root/thermeq3-install/thermeq3.zip -d /root/thermeq3 32 | if [ $? -ne 0 ]; then 33 | echo "Error during unzipping thermeq3 app: $?" 34 | exit $? 35 | fi 36 | 37 | echo "Installing libraries" 38 | opkg install python-openssl --verbosity=0 39 | if [ $? -ne 0 ]; then 40 | echo "Error during installing openssl library. Error: $?" 41 | exit $? 42 | fi 43 | 44 | if [ -d /mnt/sda1 ]; then 45 | DIR=/mnt/sda1 46 | else 47 | if [ -d /mnt/sdb1 ]; then 48 | DIR=/mnt/sdb1 49 | else 50 | echo "Please mount USB or SD card!" 51 | exit 1 52 | fi 53 | fi 54 | echo "Using $DIR as storage path." 55 | 56 | if ! grep -q "0.0.0.0:8180" /etc/config/uhttpd; then 57 | echo "Backing up uhttpd configuration..." 58 | mkdir /root/backup 59 | cp /etc/config/uhttpd /root/backup/uhttpd.old 60 | echo "Modifying uhttpd configuration..." 61 | echo "config uhttpd secondary 62 | list listen_http 0.0.0.0:8180 63 | option home $DIR/www 64 | option cgi_prefix /cgi-bin 65 | option max_requests 2 66 | option script_timeout 10 67 | option network_timeout 10 68 | option tcp_keepalive 1 69 | " >> /etc/config/uhttpd 70 | echo "Restarting uhttpd..." 71 | /etc/init.d/uhttpd restart 72 | fi 73 | 74 | echo "Creating directories and cgi scripts" 75 | mkdir -p $DIR/www 76 | cd $DIR/www 77 | mkdir -p $DIR/csv 78 | cd $DIR/www 79 | mkdir -p cgi-bin 80 | cd cgi-bin 81 | 82 | echo "Creating nsm.py compatibility file" 83 | echo "#!/usr/bin/env python 84 | import sys 85 | sys.path.insert(0, \"/root/thermeq3/\") 86 | execfile(\"/root/thermeq3/nsm.py\") 87 | " > /root/nsm.py 88 | 89 | echo "Installing scripts with $1 as device name and $DIR as target directory" 90 | echo "tail -n 50 $DIR/$1.log" > /root/ct 91 | echo "cat $DIR/$1_error.log" > /root/err 92 | echo "ps|grep python" > /root/psg 93 | echo "ps -ef | grep nsm.py | grep -v grep | awk '{print $1}' | xargs kill -9" > /root/killnsm 94 | chmod +x /root/ct 95 | chmod +x /root/err 96 | chmod +x /root/psg 97 | chmod +x /root/killnsm 98 | 99 | echo "Downloading interactive config" 100 | wget --no-check-certificate --quiet -O /root/config_me.py https://raw.githubusercontent.com/autopower/thermeq3/master/install/current/config_me.py;chmod +x /root/config_me.py 101 | if [ $? -ne 0 ]; then 102 | echo "Error during downloading config app: $?" 103 | exit $? 104 | fi 105 | echo "Downloading dashboard install script" 106 | wget --no-check-certificate --quiet -O /root/install-dash.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/dashboard/install-dash.sh;chmod +x /root/install-dash.sh 107 | if [ $? -ne 0 ]; then 108 | echo "Error during downloading dashboard install script: $?" 109 | exit $? 110 | fi 111 | echo "Dashboard install..." 112 | /root/install-dash.sh 113 | echo "Interactive config..." 114 | /root/config_me.py 115 | if [ -d /root/location.json]; then 116 | mv /root/location.json $DIR/www/location.json 117 | else 118 | echo "Can't find file. Please make location.json file for dashboard!" 119 | fi 120 | -------------------------------------------------------------------------------- /install/RPi/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ $# -lt 1 ]; then 3 | echo "Usage: install.sh " 4 | exit 1 5 | fi 6 | 7 | BASE_DIR=/home/pi/thermeq3 8 | mkdir -p $BASE_DIR 9 | mkdir -p $BASE_DIR/install 10 | 11 | echo "Downloading thermeq3 app" 12 | wget --no-check-certificate --quiet --output-document $BASE_DIR/install/thermeq3.zip https://github.com/autopower/thermeq3/raw/master/install/RPi/thermeq3.zip 13 | if [ $? -ne 0 ]; then 14 | echo "Error during downloading thermeq3 app: $?" 15 | exit $? 16 | fi 17 | mkdir $BASE_DIR/code 18 | echo "Unzipping app" 19 | unzip -q -o $BASE_DIR/install/thermeq3.zip -d $BASE_DIR/code 20 | if [ $? -ne 0 ]; then 21 | echo "Error during unzipping thermeq3 app: $?" 22 | exit $? 23 | fi 24 | 25 | echo "Installing libraries" 26 | echo "Updating apt-get and upgrading packages" 27 | sudo apt-get update 28 | sudo apt-get upgrade 29 | PKG_OK=$(dpkg-query -W --showformat='${Status}\n' python-openssl|grep "install ok installed") 30 | echo Checking for python-openssl: $PKG_OK 31 | if [ "" == "$PKG_OK" ]; then 32 | echo "No python-openssl. Installing python-openssl" 33 | sudo apt-get --force-yes --yes install python-openssl 34 | if [ $? -ne 0 ]; then 35 | echo "Error during installing openssl library. Error: $?" 36 | fi 37 | exit $? 38 | fi 39 | 40 | echo "Creating nsm.py compatibility file" 41 | echo "#!/usr/bin/env python 42 | import sys 43 | sys.path.insert(0, \"$BASE_DIR/code/\") 44 | execfile(\"$BASE_DIR/code/nsm.py\") 45 | " > $BASE_DIR/nsm.py 46 | 47 | echo "Creating systemd files" 48 | echo "[Unit] 49 | Description=thermeq3 Service 50 | After=multi-user.target 51 | 52 | [Service] 53 | Type=idle 54 | ExecStart=/usr/bin/python /home/pi/thermeq3/nsm.py 55 | 56 | [Install] 57 | WantedBy=multi-user.target" > /home/pi/thermeq3/tmp/thermeq3.service 58 | sudo mv /home/pi/thermeq3/tmp/thermeq3.service /lib/systemd/system/thermeq3.service 59 | sudo chmod 644 /lib/systemd/system/thermeq3.service 60 | sudo systemctl daemon-reload 61 | sudo systemctl enable thermeq3.service 62 | 63 | echo "Installing scripts with $1 as device name and $BASE_DIR as target directory" 64 | echo "tail -n 50 $BASE_DIR/$1.log" > $BASE_DIR/ct 65 | echo "cat $BASE_DIR/$1_error.log" > $BASE_DIR/err 66 | echo "ps|grep python" > $BASE_DIR/psg 67 | echo "ps -ef | grep nsm.py | grep -v grep | awk '{print $1}' | xargs kill -9" > $BASE_DIR/killnsm 68 | chmod +x $BASE_DIR/ct 69 | chmod +x $BASE_DIR/err 70 | chmod +x $BASE_DIR/psg 71 | chmod +x $BASE_DIR/killnsm 72 | 73 | echo "Creating folder for CSV files..." 74 | mkdir -p $BASE_DIR/csv 75 | 76 | echo "Downloading interactive config" 77 | wget --no-check-certificate --quiet --output-document $BASE_DIR/config_me.py https://raw.githubusercontent.com/autopower/thermeq3/master/install/current/config_me.py;chmod +x $BASE_DIR/config_me.py 78 | if [ $? -ne 0 ]; then 79 | echo "Error during downloading config app: $?" 80 | exit $? 81 | fi 82 | 83 | read -p "Install dahsboard? [y/n]" yn 84 | case $yn in 85 | [Yy]*) 86 | echo "Downloading dashboard install script" 87 | wget --no-check-certificate --quiet --output-document $BASE_DIR/install-dash.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/RPi/install-dash.sh;chmod +x $BASE_DIR/install-dash.sh 88 | if [ $? -ne 0 ]; then 89 | echo "Error during downloading dashboard install script: $?" 90 | exit $? 91 | fi 92 | echo "Running dashboard install..." 93 | $BASE_DIR/install-dash.sh 94 | ;; 95 | esac 96 | 97 | echo "Interactive config..." 98 | $BASE_DIR/config_me.py 99 | if [ -f $BASE_DIR/location.json ]; then 100 | echo "Moving file..." 101 | mv $BASE_DIR/location.json /var/www/html/location.json 102 | else 103 | echo "Can't find file. Please make location.json file for dashboard!" 104 | fi 105 | 106 | read -p "Delete install folder? [y/n]" yn 107 | case $yn in 108 | [Yy]*) 109 | echo "Removing install folder..." 110 | rm -rf $BASE_DIR/install/* 111 | rmdir $BASE_DIR/install 112 | ;; 113 | esac -------------------------------------------------------------------------------- /obsolete/lib/profiles.py: -------------------------------------------------------------------------------- 1 | import logmsg 2 | import datetime 3 | import time 4 | import bridge 5 | 6 | 7 | class Profile(object): 8 | def __init__(self): 9 | """ 10 | Init variables 11 | :rtype: object 12 | """ 13 | self.day = [] 14 | self.temp = [] 15 | self.selected_mode = 0 16 | self.act_mode_idx = 0 17 | self.sit = {} 18 | 19 | def start(self, dt, tt): 20 | self.day = dt 21 | self.temp = tt 22 | 23 | 24 | table = Profile() 25 | 26 | 27 | def check_day_table(): 28 | """ 29 | :return: 30 | """ 31 | global table 32 | if len(table.day) > 1: 33 | for i in range(len(table.day) - 1): 34 | if time.strptime(table.day[i + 1][0], "%H:%M") <= time.strptime(table.day[i][0], "%H:%M"): 35 | logmsg.update("Day mode table is wrong! Using default table!", 'E') 36 | table.day = [["00:00", "23:59", 35, 185, "total", 120, 2]] 37 | 38 | 39 | def time_in_range(start, end, x): 40 | """ 41 | :param start: 42 | :param end: 43 | :param x: 44 | :return: 45 | """ 46 | today = datetime.date.today() 47 | start = datetime.datetime.combine(today, start) 48 | end = datetime.datetime.combine(today, end) 49 | x = datetime.datetime.combine(today, x) 50 | if end <= start: 51 | end += datetime.timedelta(1) # tomorrow! 52 | if x <= start: 53 | x += datetime.timedelta(1) # tomorrow! 54 | return start <= x <= end 55 | 56 | 57 | def is_time(): 58 | """ 59 | :return: 60 | """ 61 | global table 62 | this_now = datetime.datetime.now().time() 63 | ret_value = -1 64 | for k in table.day: 65 | nf = datetime.datetime.strptime(k[0], "%H:%M").time() 66 | nt = datetime.datetime.strptime(k[1], "%H:%M").time() 67 | if time_in_range(nf, nt, this_now): 68 | ret_value = table.day.index(k) 69 | # logmsg.update("IsTime=" + str(ret_value)) 70 | return ret_value 71 | 72 | 73 | def time_mode(): 74 | """ 75 | :return: 76 | """ 77 | global table 78 | # day = [0-from_str, 1-to_str, 2-total or per, 3-mode ("total"/"per"), 4-check interval, 5-valves] 79 | md = is_time() 80 | # logmsg.update("Actual index=" + str(table.act_mode_idx), 'D') 81 | if md != -1: 82 | if md != table.act_mode_idx: 83 | kv = table.day[md] 84 | table.act_mode_idx = md 85 | logmsg.update("Switching day mode to " + str(md) + " = " + str(kv), 'I') 86 | return kv 87 | 88 | 89 | def temp_mode(): 90 | """ 91 | :return: 92 | """ 93 | global table 94 | if None not in table.sit.viewvalues(): 95 | c_temp = int(table.sit["current_temp"]) 96 | for k in range(0, len(table.temp)): 97 | kv = table.temp[k] 98 | if kv[1] > c_temp >= kv[0]: 99 | table.act_mode_idx = k 100 | logmsg.update("Switching temp mode to " + str(kv[0]) + " ~ " + str(kv[1]), 'I') 101 | return kv 102 | else: 103 | logmsg.update("Weather situation error", 'E') 104 | 105 | 106 | def do(sel_mode, act_idx, sit): 107 | """ 108 | Performs day or temp mode selection 109 | :param sel_mode: char, selected mode 110 | :param act_idx: integer, actual index 111 | :param sit: dictionary, weather situation 112 | :return: list 113 | """ 114 | # update variables from main code 115 | global table 116 | table.selected_mode = sel_mode 117 | table.act_mode_idx = act_idx 118 | table.sit = sit 119 | tmp_prof = bridge.try_read("profile", True).upper() 120 | kv = [] 121 | if tmp_prof != table.selected_mode: 122 | table.selected_mode = tmp_prof 123 | table.act_mode_idx = -1 124 | if tmp_prof == "TIME": 125 | check_day_table() 126 | kv = time_mode() 127 | elif tmp_prof == "TEMP": 128 | kv = temp_mode() 129 | 130 | return [table.selected_mode, table.act_mode_idx, kv] 131 | 132 | 133 | def init(dt, tt): 134 | """ 135 | Init day and temp table 136 | :param dt: list, daytable 137 | :param tt: list, daytable 138 | :return: nothing 139 | """ 140 | global table 141 | table.start(dt, tt) 142 | -------------------------------------------------------------------------------- /obsolete/lib/weather.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import urllib2 3 | import logmsg 4 | import traceback 5 | from math import exp 6 | import json 7 | 8 | 9 | def weather_for_woeid(woeid, owm_id, owm_api_key): 10 | """ 11 | Returns weather from yahoo weather from given WOEID 12 | :param owm_id: 13 | :param owm_api_key: 14 | :param woeid: integer, yahoo weather ID 15 | :return: dictionary, {"current_temp": temp, "city": city, "humidity": humidity} 16 | """ 17 | city = "Error city" 18 | temp = None 19 | humidity = None 20 | 21 | if woeid is None or owm_id is None: 22 | logmsg.update("Wrong WOEID! Please set WOEID in config.py.", 'E') 23 | elif owm_api_key is None: 24 | logmsg.update("OWM API key not set!", 'E') 25 | else: 26 | # please change u='c' to u='f' for fahrenheit below 27 | base_url = "https://query.yahooapis.com/v1/public/yql?" 28 | yql_query = "select * from weather.forecast where woeid=" + str(woeid) + " and u='c'" 29 | yql_url = base_url + urllib.urlencode({'q': yql_query}) + "&format=json" 30 | 31 | try: 32 | result = urllib2.urlopen(yql_url).read() 33 | data = json.loads(result) 34 | except Exception, error: 35 | logmsg.update("Yahoo communication error: " + str(error), 'E') 36 | logmsg.update("Traceback: " + str(traceback.format_exc()), 'E') 37 | else: 38 | if data is not None: 39 | try: 40 | city = data["query"]["results"]["channel"]["location"]["city"] 41 | temp = int(data["query"]["results"]["channel"]["item"]["condition"]["temp"]) 42 | humidity = int(data["query"]["results"]["channel"]["atmosphere"]["humidity"]) 43 | except Exception: 44 | pass 45 | else: 46 | # and check if yahoo is correct 47 | url = "http://api.openweathermap.org/data/2.5/weather?id=" + str(owm_id) + "&appid=" + \ 48 | owm_api_key + "&units=metric" 49 | try: 50 | result = json.load(urllib2.urlopen(url)) 51 | except Exception, error: 52 | logmsg.update("OWM communication error: " + str(error), 'E') 53 | logmsg.update("Traceback: " + str(traceback.format_exc()), 'E') 54 | else: 55 | if "main" in result: 56 | owm_temp = round(result["main"]["temp"]) 57 | yho_temp = round(temp) 58 | if abs(yho_temp - owm_temp) > 1.5: 59 | logmsg.update("Difference between Yahoo and OWM temperatures. Yahoo=" + str(yho_temp) + 60 | " OWM=" + str(owm_temp), 'I') 61 | # end check 62 | else: 63 | logmsg.update("Error during parsing result.", 'D') 64 | logmsg.update( 65 | "Current temperature in " + str(city) + " is " + str(temp) + ", humidity " + str(humidity) + 66 | "%", 'I') 67 | 68 | return { 69 | "current_temp": temp, 70 | "city": city, 71 | "humidity": humidity 72 | } 73 | 74 | 75 | def _scale(val, src, dst): 76 | """ Scale the given value from the scale of src to the scale of dst 77 | :param val: float, value to scale 78 | :param src: list, scale interval from 79 | :param dst: list, scale interval to 80 | :return: float 81 | """ 82 | return ((val - src[0]) / (src[1] - src[0])) * (dst[1] - dst[0]) + dst[0] 83 | 84 | 85 | def interval_scale(temp, t, r, w, test): 86 | """ 87 | :param temp: int, temperature 88 | :param t: list, temperature min, max range 89 | :param r: list, min, max for exp() 90 | :param w: list, target range, min max 91 | :param test: boolean, test for range violation 92 | :return: int, mapped value 93 | """ 94 | if test: 95 | if temp < t[0]: 96 | temp = t[0] 97 | elif temp > t[1]: 98 | temp = t[1] 99 | 100 | a = _scale(temp, t, r) 101 | b = int(_scale(exp(a), (exp(r[0]), exp(r[1])), w)) 102 | 103 | return b 104 | 105 | -------------------------------------------------------------------------------- /obsolete/lib/autoupdate.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | import traceback 3 | import httplib 4 | import logmsg 5 | import os 6 | import hashlib 7 | import zipfile 8 | 9 | 10 | def get_hash(filename): 11 | checksum = hashlib.md5() 12 | if os.path.isfile(filename): 13 | f = file(filename, "rb") 14 | while True: 15 | part = f.read(1024) 16 | if not part: 17 | break 18 | checksum.update(part) 19 | f.close() 20 | return checksum 21 | 22 | 23 | def download_file(get_file, put_file): 24 | err_str = "" 25 | try: 26 | request = urllib2.urlopen(get_file) 27 | response = request.read() 28 | except urllib2.HTTPError, e: 29 | err_str += "HTTPError = " + str(e.reason) 30 | except urllib2.URLError, e: 31 | err_str += "URLError = " + str(e.reason) 32 | except httplib.HTTPException: 33 | err_str += "HTTPException" 34 | except Exception, e: 35 | err_str += "Exception = " + str(traceback.format_exc()) + ". Error: " + str(e) 36 | else: 37 | tmp_dir = os.path.dirname(put_file) 38 | if not os.path.exists(tmp_dir): 39 | os.mkdir(tmp_dir) 40 | try: 41 | f = file(put_file, "wb") 42 | except Exception, e: 43 | err_str = "Problem during saving new version. File: " + put_file + ". Error: " + str( 44 | e) + " Traceback: " + str(traceback.format_exc()) 45 | else: 46 | f.write(response) 47 | f.close() 48 | err_str = "" 49 | request.close() 50 | finally: 51 | if not err_str == "": 52 | logmsg.update(err_str) 53 | return False 54 | return True 55 | 56 | 57 | def check_update(version): 58 | github = "https://github.com/autopower/thermeq3/raw/master/install/current/" 59 | home_dir = "/root/thermeq3" 60 | err_str = "Unable to get latest version info - " 61 | 62 | try: 63 | request = urllib2.urlopen(github + "autoupdate.data") 64 | response = request.read().rstrip("\r\n") 65 | except urllib2.HTTPError, e: 66 | err_str += "HTTPError = " + str(e.reason) 67 | except urllib2.URLError, e: 68 | err_str += "URLError = " + str(e.reason) 69 | except httplib.HTTPException: 70 | err_str += "HTTPException" 71 | except Exception: 72 | err_str += "Exception = " + str(traceback.format_exc()) 73 | else: 74 | err_str = "" 75 | t = response.split(":") 76 | 77 | new_hash = get_hash(home_dir + "-install/" + str(t[1])).hexdigest() 78 | if new_hash == "": 79 | logmsg.update("Can't find file " + str(t[1]), 'E') 80 | else: 81 | try: 82 | tmp_ver = int(t[0]) 83 | except Exception: 84 | tmp_ver = 0 85 | logmsg.update("Available file: " + str(t[1]) + ", V" + str(tmp_ver) + " with hash " + str(t[3]), 'I') 86 | logmsg.update("Actual version: " + str(version) + ", hash: " + str(new_hash), 'I') 87 | if new_hash != t[3] and version < tmp_ver: 88 | logmsg.update("Downloading new version V" + str(tmp_ver)) 89 | down_result = download_file(github + t[1], home_dir + "-install/" + t[1]) 90 | if down_result: 91 | logmsg.update("V" + str(tmp_ver) + " downloaded. Hash is " + str(t[3])) 92 | return [2, t[1]] 93 | else: 94 | logmsg.update("Problem downloading new version. Result=" + str(down_result) + ", file=" + str(t[1])) 95 | else: 96 | return [1, ""] 97 | 98 | if not err_str == "": 99 | logmsg.update(err_str) 100 | return [0, ""] 101 | 102 | 103 | def do(version): 104 | """ 105 | Perform update 106 | :param version: string 107 | :return: boolean, True if something updated 108 | """ 109 | home_dir = "/root/thermeq3" 110 | chk, filename = check_update(version) 111 | result = False 112 | if chk == 2: 113 | # unzip files 114 | with zipfile.ZipFile(home_dir + "-install/" + filename, "r") as z: 115 | try: 116 | z.extractall(home_dir + "/") 117 | except Exception: 118 | logmsg.update("Error during archive extraction", 'E') 119 | else: 120 | logmsg.update("Archive successfully extracted.", 'I') 121 | result = True 122 | elif chk == 1: 123 | logmsg.update("Update is not necessary.") 124 | return result 125 | -------------------------------------------------------------------------------- /yun-sketch/thermeq3_V200/thermeq3_V200.ino: -------------------------------------------------------------------------------- 1 | // #define DEBUG_PRG 2 | 3 | #include 4 | 5 | #define RELAY_PIN 10 6 | #define STATUS_LED 9 7 | #define ERROR_LED 8 8 | #define LOOP_LED 13 9 | #define BLINK_INTERVAL 150 10 | #define WAIT_UPDATE_SYNC 10000 11 | 12 | #define IWANNABESAFE 13 | 14 | // define this if you wanna use some optocoupler for relay or reverse logic 15 | // #define REVERSE_LOGIC 16 | #ifdef REVERSE_LOGIC 17 | #define LOGIC_1 LOW 18 | #define LOGIC_0 HIGH 19 | #else 20 | #define LOGIC_1 HIGH 21 | #define LOGIC_0 LOW 22 | #endif 23 | 24 | Process p; 25 | char bridgeBuffer[16]; 26 | boolean sysStatus = false; 27 | unsigned long waitUntil = 0; 28 | unsigned long interval = 10*1000; // interval in seconds, change 10 to anything you want 29 | unsigned long app_interval = 10*60000; // interval in minutes, change 10 to anything you want 30 | unsigned long waitForApp = 0; 31 | 32 | void alloff() { 33 | digitalWrite(LOOP_LED, LOW); 34 | digitalWrite(STATUS_LED, LOW); 35 | digitalWrite(ERROR_LED, LOW); 36 | } 37 | 38 | void allon() { 39 | digitalWrite(LOOP_LED, HIGH); 40 | digitalWrite(STATUS_LED, HIGH); 41 | digitalWrite(ERROR_LED, HIGH); 42 | } 43 | 44 | void blinkLED() { 45 | for (byte i = 0; i < 4; i++) { 46 | allon(); 47 | delay(BLINK_INTERVAL); 48 | alloff(); 49 | delay(BLINK_INTERVAL); 50 | } 51 | } 52 | 53 | void turnIt(boolean onoff) { 54 | showError(false); 55 | if (sysStatus == onoff) return; 56 | 57 | if (onoff) { 58 | digitalWrite(RELAY_PIN, LOGIC_1); 59 | digitalWrite(STATUS_LED, HIGH); 60 | } else { 61 | digitalWrite(RELAY_PIN, LOGIC_0); 62 | digitalWrite(STATUS_LED, LOW); 63 | } 64 | sysStatus = onoff; 65 | Bridge.put("msg", ""); 66 | } 67 | 68 | void fatalError() { 69 | turnIt(false); 70 | alloff(); 71 | while (true) { 72 | digitalWrite(LOOP_LED, HIGH); 73 | delay(BLINK_INTERVAL); 74 | digitalWrite(LOOP_LED, LOW); 75 | digitalWrite(STATUS_LED, HIGH); 76 | delay(BLINK_INTERVAL); 77 | digitalWrite(STATUS_LED, LOW); 78 | digitalWrite(ERROR_LED, HIGH); 79 | delay(BLINK_INTERVAL); 80 | digitalWrite(ERROR_LED, LOW); 81 | } 82 | } 83 | 84 | void showDead() { 85 | turnIt(false); 86 | alloff(); 87 | while (true) { 88 | for(int j = 0 ; j <= 255; j +=5) { 89 | analogWrite(STATUS_LED, j); 90 | delay(30); 91 | } 92 | for(int j = 255 ; j >= 0; j -=10) { 93 | analogWrite(STATUS_LED, j); 94 | delay(30); 95 | } 96 | digitalWrite(STATUS_LED, LOW); 97 | delay(BLINK_INTERVAL * 5); 98 | } 99 | } 100 | 101 | void showError(boolean onoff) { 102 | if (onoff) digitalWrite(ERROR_LED, HIGH); 103 | else digitalWrite(ERROR_LED, LOW); 104 | Bridge.put("msg", ""); 105 | } 106 | 107 | void runApp() { 108 | turnIt(false); 109 | // you can use p.begin("/root/nsm.py") without .addParameter 110 | p.begin("python"); 111 | p.addParameter("/root/thermeq3/nsm.py"); 112 | p.runAsynchronously(); 113 | } 114 | 115 | void setup() { 116 | #ifdef DEBUG_PRG 117 | Serial.begin(9600); 118 | while (!Serial); 119 | #endif 120 | 121 | pinMode(RELAY_PIN, OUTPUT); 122 | pinMode(ERROR_LED, OUTPUT); 123 | pinMode(STATUS_LED, OUTPUT); 124 | pinMode(LOOP_LED, OUTPUT); 125 | 126 | blinkLED(); 127 | allon(); 128 | Bridge.begin(); 129 | blinkLED(); 130 | 131 | runApp(); 132 | 133 | #ifdef DEBUG_PRG 134 | Serial.println("GO!"); 135 | Serial.println(interval); 136 | Serial.println(app_interval); 137 | #endif 138 | } 139 | 140 | void loop() { 141 | unsigned long mls = millis(); 142 | 143 | if (mls - waitUntil >= interval) { 144 | digitalWrite(LOOP_LED, HIGH); 145 | Bridge.get("msg", bridgeBuffer, 16); 146 | char c = bridgeBuffer[0]; 147 | 148 | switch (c) { 149 | case 'H': 150 | turnIt(true); 151 | break; 152 | case 'S': 153 | turnIt(false); 154 | break; 155 | case 'E': 156 | #ifdef IWANNABESAFE 157 | turnIt(false); 158 | #endif 159 | showError(true); 160 | break; 161 | case 'A': 162 | alloff(); 163 | break; 164 | case 'Q': 165 | fatalError(); 166 | break; 167 | case 'D': 168 | turnIt(false); 169 | showDead(); 170 | break; 171 | case 'R': 172 | #ifdef IWANNABESAFE 173 | turnIt(false); 174 | #endif 175 | digitalWrite(ERROR_LED, LOW); 176 | digitalWrite(STATUS_LED, LOW); 177 | digitalWrite(ERROR_LED, HIGH); 178 | digitalWrite(STATUS_LED, HIGH); 179 | p.close(); 180 | delay(WAIT_UPDATE_SYNC); 181 | runApp(); 182 | digitalWrite(ERROR_LED, LOW); 183 | digitalWrite(STATUS_LED, LOW); 184 | break; 185 | } 186 | waitUntil = waitUntil + interval; 187 | digitalWrite(LOOP_LED, LOW); 188 | } 189 | 190 | if (mls - waitForApp >= app_interval) { 191 | #ifdef DEBUG_PRG 192 | Serial.println(mls); 193 | Serial.println(p.running()); 194 | #endif 195 | digitalWrite(LOOP_LED, HIGH); 196 | if (p.running() == false) { 197 | p.close(); 198 | runApp(); 199 | } 200 | waitForApp = waitForApp + app_interval; 201 | digitalWrite(LOOP_LED, LOW); 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /yun-sketch/thermeq3/thermeq3.ino: -------------------------------------------------------------------------------- 1 | // #define DEBUG_PRG 2 | 3 | #include 4 | 5 | #define RELAY_PIN 10 6 | // #define RELAY_POWER 8 7 | #define STATUS_LED 9 8 | #define ERROR_LED 8 9 | #define LOOP_LED 13 10 | #define BLINK_INTERVAL 150 11 | #define WAIT_UPDATE_SYNC 10000 12 | 13 | #define IWANNABESAFE 14 | 15 | Process p; 16 | char bridgeBuffer[16]; 17 | boolean sysStatus = false; 18 | unsigned long waitUntil = 0; 19 | unsigned long interval = 10*1000; // interval in seconds, change 10 to anything you want 20 | unsigned long app_interval = 5*60000; // interval in minutes, change 5 to anything you want 21 | unsigned long waitForApp = 0; 22 | 23 | void blinkLED() { 24 | for (byte i = 0; i < 4; i++) { 25 | digitalWrite(LOOP_LED, HIGH); 26 | digitalWrite(STATUS_LED, HIGH); 27 | digitalWrite(ERROR_LED, HIGH); 28 | delay(BLINK_INTERVAL); 29 | digitalWrite(LOOP_LED, LOW); 30 | digitalWrite(STATUS_LED, LOW); 31 | digitalWrite(ERROR_LED, LOW); 32 | delay(BLINK_INTERVAL); 33 | } 34 | } 35 | 36 | void turnIt(boolean onoff) { 37 | showError(false); 38 | if (sysStatus == onoff) return; 39 | 40 | if (onoff) { 41 | #ifdef RELAY_POWER 42 | digitalWrite(RELAY_POWER, HIGH); 43 | delay(500); 44 | #endif 45 | digitalWrite(RELAY_PIN, HIGH); 46 | digitalWrite(STATUS_LED, HIGH); 47 | } else { 48 | digitalWrite(RELAY_PIN, LOW); 49 | #ifdef RELAY_POWER 50 | delay(500); 51 | digitalWrite(RELAY_POWER, LOW); 52 | #endif 53 | digitalWrite(STATUS_LED, LOW); 54 | } 55 | sysStatus = onoff; 56 | Bridge.put("msg", ""); 57 | } 58 | 59 | void fatalError() { 60 | turnIt(false); 61 | digitalWrite(LOOP_LED, LOW); 62 | digitalWrite(STATUS_LED, LOW); 63 | digitalWrite(ERROR_LED, LOW); 64 | while (true) { 65 | digitalWrite(LOOP_LED, HIGH); 66 | delay(BLINK_INTERVAL); 67 | digitalWrite(LOOP_LED, LOW); 68 | digitalWrite(STATUS_LED, HIGH); 69 | delay(BLINK_INTERVAL); 70 | digitalWrite(STATUS_LED, LOW); 71 | digitalWrite(ERROR_LED, HIGH); 72 | delay(BLINK_INTERVAL); 73 | digitalWrite(ERROR_LED, LOW); 74 | } 75 | } 76 | 77 | void showDead() { 78 | turnIt(false); 79 | digitalWrite(LOOP_LED, LOW); 80 | digitalWrite(STATUS_LED, LOW); 81 | digitalWrite(ERROR_LED, LOW); 82 | while (true) { 83 | for(int j = 0 ; j <= 255; j +=5) { 84 | analogWrite(STATUS_LED, j); 85 | delay(30); 86 | } 87 | 88 | for(int j = 255 ; j >= 0; j -=10) { 89 | analogWrite(STATUS_LED, j); 90 | delay(30); 91 | } 92 | digitalWrite(STATUS_LED, LOW); 93 | delay(BLINK_INTERVAL * 5); 94 | } 95 | } 96 | 97 | void showError(boolean onoff) { 98 | if (onoff) digitalWrite(ERROR_LED, HIGH); 99 | else digitalWrite(ERROR_LED, LOW); 100 | Bridge.put("msg", ""); 101 | } 102 | 103 | void runApp() { 104 | turnIt(false); 105 | // start python app 106 | p.begin("python"); 107 | p.addParameter("/root/thermeq3/nsm.py"); 108 | p.runAsynchronously(); 109 | } 110 | 111 | void signalReadNotOK() { 112 | turnIt(false); 113 | for (byte i = 0; i < 4; i++) { 114 | digitalWrite(ERROR_LED, HIGH); 115 | delay(BLINK_INTERVAL); 116 | digitalWrite(ERROR_LED, LOW); 117 | delay(BLINK_INTERVAL); 118 | } 119 | } 120 | 121 | void setup() { 122 | #ifdef DEBUG_PRG 123 | Serial.begin(9600); 124 | while (!Serial); 125 | #endif 126 | 127 | pinMode(RELAY_PIN, OUTPUT); 128 | #ifdef RELAY_POWER 129 | pinMode(RELAY_POWER, OUTPUT); 130 | #endif 131 | pinMode(ERROR_LED, OUTPUT); 132 | pinMode(STATUS_LED, OUTPUT); 133 | pinMode(LOOP_LED, OUTPUT); 134 | 135 | blinkLED(); 136 | digitalWrite(STATUS_LED, HIGH); 137 | digitalWrite(ERROR_LED, HIGH); 138 | digitalWrite(LOOP_LED, HIGH); 139 | Bridge.begin(); 140 | blinkLED(); 141 | 142 | runApp(); 143 | 144 | #ifdef DEBUG_PRG 145 | Serial.println("GO!"); 146 | Serial.println(interval); 147 | Serial.println(app_interval); 148 | #endif 149 | } 150 | 151 | void loop() { 152 | unsigned long mls = millis(); 153 | 154 | if (mls - waitUntil >= interval) { 155 | digitalWrite(LOOP_LED, HIGH); 156 | Bridge.get("msg", bridgeBuffer, 16); 157 | char c = bridgeBuffer[0]; 158 | 159 | switch (c) { 160 | case 'H': 161 | turnIt(true); 162 | break; 163 | case 'S': 164 | turnIt(false); 165 | break; 166 | case 'E': 167 | // in case that cube is not available, e.g. timeout 168 | #ifdef IWANNABESAFE 169 | turnIt(false); 170 | #endif 171 | showError(true); 172 | break; 173 | case 'C': 174 | showError(false); 175 | break; 176 | case 'Q': 177 | fatalError(); 178 | break; 179 | case 'D': 180 | turnIt(false); 181 | showDead(); 182 | break; 183 | case 'M': 184 | // in case that read from max cube malformed 185 | signalReadNotOK(); 186 | break; 187 | case 'R': 188 | #ifdef IWANNABESAFE 189 | turnIt(false); 190 | #endif 191 | digitalWrite(ERROR_LED, HIGH); 192 | digitalWrite(STATUS_LED, HIGH); 193 | p.close(); 194 | delay(WAIT_UPDATE_SYNC); 195 | runApp(); 196 | break; 197 | } 198 | waitUntil = waitUntil + interval; 199 | digitalWrite(LOOP_LED, LOW); 200 | } 201 | 202 | if (mls - waitForApp >= app_interval) { 203 | #ifdef DEBUG_PRG 204 | Serial.println(mls); 205 | Serial.println(p.running()); 206 | #endif 207 | digitalWrite(LOOP_LED, HIGH); 208 | if (p.running() == false) { 209 | p.close(); 210 | runApp(); 211 | } 212 | waitForApp = waitForApp + app_interval; 213 | digitalWrite(LOOP_LED, LOW); 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /obsolete/lib/mailer.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import logmsg 3 | import os 4 | import traceback 5 | from email.mime.multipart import MIMEMultipart 6 | from email.mime.text import MIMEText 7 | from email.mime.base import MIMEBase 8 | from email.encoders import encode_base64 9 | import thermeq3 10 | 11 | subject_body = {"battery": ["Battery status for device %(b0)s. Warning from thermeq3 device", 12 | """

Device %(a0)s battery status warning.

13 |

Hello, I'm your thermostat and I have a warning for you.
14 | Please take a care of device %(a0)s in room %(a1)s. 15 | This device have low batteries, please replace batteries.
16 |

You can mute this warning for %(a3)s mins."""], 17 | "error": ["Error report for device %(a0)s. Warning from thermeq3 device", 18 | """

Device %(a0)s radio error.

19 |

Hello, I'm your thermostat and I have a warning for you.
20 | Please take a care of device %(a0)s in room %(a1)s. 21 | This device reports error.
22 |

You can mute this warning for %(a3)s mins."""], 23 | "openmax": ["Can't connect to MAX! Cube! Warning from thermeq3 device", ""], 24 | "upgrade": ["thermeq3 device is going to be upgraded", ""], 25 | "window": ["Open window in room %(b0)s. Warning from thermeq3 device", 26 | """

Device %(a0)s warning.

27 |

Hello, I'm your thermostat and I have a warning for you.
28 | Please take a care of window %(a0)s in room %(a1)s. 29 | Window in this room is now opened more than %(a2)s.
30 | Threshold for warning is %(a3)d mins.
31 |

You can mute this warning for %(a5)s mins."""] 32 | } 33 | 34 | 35 | def new_compose(selector, b0, a0, a1="", a2="", a3=0, a4="", a5=0): 36 | global subject_body 37 | subject = subject_body[selector][0] % {'b0': str(b0)} 38 | if selector == "window": 39 | body = subject_body[selector][1] % {'a0': str(a0), 'a1': str(a1), 'a2': str(a2), 'a3': int(a3), 40 | 'a4': str(a4), 'a5': int(a5)} 41 | elif selector == "openmax" or selector == "upgrade": 42 | body = a0 43 | else: 44 | body = subject_body[selector][1] % {'a0': str(a0), 'a1': str(a1), 'a2': str(a2), 'a3': int(a3)} 45 | 46 | c_msg = MIMEMultipart() 47 | t3_setup = thermeq3.t3.setup 48 | c_msg["From"] = '"' + t3_setup.device_name + '" <' + t3_setup.from_addr + ">" 49 | c_msg["To"] = t3_setup.to_addr if isinstance(t3_setup.to_addr, basestring) else ','.join(t3_setup.to_addr) 50 | c_msg["Subject"] = subject 51 | body = """""" + body + "

" 52 | c_msg.attach(MIMEText(body, "html")) 53 | 54 | return c_msg 55 | 56 | 57 | def expand_address_list(address_list): 58 | return address_list if isinstance(address_list, basestring) else ','.join(address_list) 59 | 60 | 61 | def str_to_list(tmp_str): 62 | tmp = tmp_str.replace("[", "").replace("]", "") 63 | return tmp.split(",") 64 | 65 | 66 | def compose(eq3_setup, subject, body): 67 | """ 68 | Compose message 69 | :param eq3_setup: thermeq3.setup 70 | :param subject: string 71 | :param body: string 72 | :return: MIMEMultipart 73 | """ 74 | c_msg = MIMEMultipart() 75 | c_msg["From"] = '"' + str(eq3_setup.device_name) + '" <' + str(eq3_setup.from_addr) + ">" 76 | c_msg["To"] = expand_address_list(eq3_setup.to_addr) 77 | c_msg["Subject"] = subject 78 | body = """""" + body + "

" 79 | c_msg.attach(MIMEText(body, "html")) 80 | 81 | return c_msg 82 | 83 | 84 | def attach_file(filename): 85 | """ 86 | Attach filename 87 | :param filename: string 88 | :return: MIMEBase 89 | """ 90 | part = MIMEBase("application", "octet-stream") 91 | part.set_payload(open(filename, "rb").read()) 92 | encode_base64(part) 93 | basename = os.path.basename(filename) 94 | attachment_filename = os.path.splitext(basename)[0] 95 | # for better attachment handling and reading on mobile devices 96 | part.add_header("Content-Disposition", "attachment; filename=\"" + attachment_filename + ".txt\"\"") 97 | return part 98 | 99 | 100 | def send_error_log(eq3_setup, stderr_log): 101 | """ 102 | Send error log if any 103 | :param eq3_setup: thermeq3.setup 104 | :param stderr_log: string 105 | :return: -1 if no error log 106 | """ 107 | if not (not os.path.isfile(stderr_log) or not (os.path.getsize(stderr_log) > 0)): 108 | subject = eq3_setup.device_name + " log email (thermeq3 device)" 109 | body = ("

%(a0)s status email.

\n" 110 | "

Hello, I'm your thermostat.
" 111 | " I found this error log. It's attached.
") % {"a0": str(eq3_setup.device_name)} 112 | msg = compose(eq3_setup, subject, body) 113 | msg.attach(attach_file(stderr_log)) 114 | return send_email(eq3_setup, msg.as_string()) 115 | else: 116 | logmsg.update("Logfile: " + stderr_log) 117 | logmsg.update("Zero sized stderr log file, nothing'll be send") 118 | return False 119 | 120 | 121 | def send_email(eq3_setup, message): 122 | """ 123 | sends email 124 | :param eq3_setup: thermeq3.setup 125 | :param message: 126 | :return: boolean, true if success 127 | """ 128 | try: 129 | server = smtplib.SMTP(eq3_setup.mail_server, eq3_setup.mail_port) 130 | except Exception, error: 131 | logmsg.update( 132 | "Error connecting to mail server " + str(eq3_setup.mail_server) + ":" + str( 133 | eq3_setup.mail_port) + ". Error code: " + str(error)) 134 | logmsg.update("Traceback: " + str(traceback.format_exc())) 135 | else: 136 | try: 137 | server.ehlo() 138 | if server.has_extn('STARTTLS'): 139 | server.starttls() 140 | server.ehlo() 141 | server.login(eq3_setup.from_addr, eq3_setup.from_pwd) 142 | tmp = str_to_list(eq3_setup.to_addr) 143 | server.sendmail(eq3_setup.from_addr, str_to_list(eq3_setup.to_addr), message) 144 | except smtplib.SMTPAuthenticationError: 145 | logmsg.update("Authentication error during sending email.") 146 | except smtplib.SMTPRecipientsRefused: 147 | logmsg.update("Recipient refused.") 148 | except Exception, error: 149 | logmsg.update("Error during sending email. Error code: " + str(error)) 150 | else: 151 | server.quit() 152 | logmsg.update("Mail was sent.") 153 | return True 154 | return False 155 | -------------------------------------------------------------------------------- /obsolete/lib/bridge.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logmsg 4 | import json 5 | import support 6 | import time 7 | 8 | 9 | fake_bridge = True 10 | 11 | support.guess_platform() 12 | if support.is_yun(): 13 | sys.path.insert(0, "/usr/lib/python2.7/bridge/") 14 | try: 15 | from bridgeclient import BridgeClient 16 | except ImportError: 17 | raise ImportError("Error importing from BridgeClient library.") 18 | else: 19 | fake_bridge = False 20 | 21 | 22 | if fake_bridge and not support.is_yun(): 23 | # if exception = no yun, abstraction to bridge, simplified code inspired by arduino yun 24 | # abstract bridge client 25 | class BridgeClient: 26 | def __init__(self): 27 | self.bridge_var = {} 28 | self.fake = True 29 | 30 | def get(self, key): 31 | if key in self.bridge_var: 32 | r = self.bridge_var[key] 33 | else: 34 | r = None 35 | return r 36 | 37 | def getall(self): 38 | return self.bridge_var 39 | 40 | def put(self, key, value): 41 | self.bridge_var.update({key: value}) 42 | 43 | # run httpd server here 44 | # end of abstraction 45 | 46 | bridge_client = BridgeClient() 47 | 48 | 49 | # required values, if any error in bridge then defaults is used [1] 50 | # format codeword: ["codeword in bridge", default value, literal processing, variable] 51 | cw = { 52 | "mode": ["mode", "auto", True, "var.mode"], 53 | # valve position in % to start heating 54 | "valve": ["valve_pos", 35, True, "setup.valve_pos"], 55 | # start heating if single valve position in %, no matter how many valves are needed to start to heating 56 | "svpnmw": ["svpnmw", 75, True, "setup.svpnmw"], 57 | # how many valves must be in position stated above 58 | "valves": ["valves", 2, True, "setup.valves"], 59 | # if in total mode, sum of valves position to start heating 60 | "total": ["total_switch", 150, True, "setup.total_switch"], 61 | # preference, "per" = per valve, "total" to total mode 62 | "pref": ["preference", "per", True, "setup.preference"], 63 | # interval, seconds to read MAX!Cube 64 | "int": ["interval", 90, True, "setup.intervals['max'][0]"], 65 | # 66 | "ign_op": ["ignore_opened", 15, True, "eq3.ignore_time"], 67 | # use auto update function? 68 | "au": ["autoupdate", True, True, "setup.au"], 69 | # beta features on (yes) or off (no) 70 | "beta": ["beta", "no", True, "var.beta"], 71 | # profile type, time or temp, temp profile type means that external temperature (yahoo weather) is used 72 | "profile": ["profile", "normal", False, ""], 73 | # list of ignored devices 74 | "ign": ["ignored", {}, True, "eq3.ignored_valves"], 75 | # no open window warning, if True then no window warning via email 76 | "no_oww": ["no_oww", 0, True, "setup.no_oww"], 77 | # heat time dictionary 78 | "ht": ["heattime", {}, True, "var.ht"], 79 | # communication errors, how many times failed communication between thermeq3 and MAX!Cube, 0 after sending status 80 | "errs": ["error", 0, False, ""], 81 | # same as above, but cumulative number 82 | "terrs": ["totalerrors", 0, False, ""], 83 | "cmd": ["command", "", False, ""], 84 | "msg": ["msg", "", False, ""], 85 | "uptime": ["uptime", "", False, ""], 86 | "appuptime": ["app_uptime", 0, False, ""], 87 | "status": ["status", "defaults", False, ""], 88 | "sys": ["system_status", {}, False, ""], 89 | "owl": ["open_window_list", {}, False, ""] 90 | } 91 | 92 | 93 | def get_pcw(): 94 | global cw 95 | lcw = {} 96 | for k, v in cw.iteritems(): 97 | # key : [default, literal] 98 | lcw.update({v[0]: [v[1], v[2], v[3]]}) 99 | return lcw 100 | 101 | 102 | pcw = get_pcw() 103 | 104 | 105 | def save(bridge_file): 106 | """ 107 | Save bridge to bridge_file, if success return True, else False 108 | :param bridge_file: string 109 | :return: boolean 110 | """ 111 | global bridge_client 112 | try: 113 | tmp = bridge_client.getall() 114 | except ValueError: 115 | logmsg.update("Error reading bridge!", 'E') 116 | else: 117 | try: 118 | f = open(bridge_file, "w") 119 | except IOError: 120 | logmsg.update("Error writing to bridge file!", 'E') 121 | else: 122 | f.write(json.dumps(tmp, sort_keys=True)) 123 | f.close() 124 | logmsg.update("Bridge file (" + str(bridge_file) + ") saved.", 'D') 125 | return True 126 | return False 127 | 128 | 129 | def load(bridge_file): 130 | """ 131 | Load data from bridge_file and return dictionary or None 132 | :param bridge_file: string 133 | :return: dictionary 134 | """ 135 | data = {} 136 | if os.path.exists(bridge_file): 137 | with open(bridge_file, "r") as f: 138 | try: 139 | data = json.load(f) 140 | except ValueError: 141 | logmsg.update("Bridge value error during loading bridge!", 'E') 142 | finally: 143 | f.close() 144 | logmsg.update("Bridge file loaded.", 'D') 145 | else: 146 | logmsg.update("Error loading bridge file, file not exist!", 'E') 147 | # load empty dict, not None 148 | # data = None 149 | data = {} 150 | return data 151 | 152 | 153 | def get_cw(lcw): 154 | """ 155 | Return codeword from dictionary 156 | :param lcw: key 157 | :return: string 158 | """ 159 | global cw 160 | if lcw in cw: 161 | return str(cw[lcw][0]) 162 | else: 163 | return "wrong_key " + str(lcw) 164 | 165 | 166 | def get_cw_default(lcw): 167 | """ 168 | Return codeword and default value from dictionary 169 | :param lcw: key 170 | :return: string 171 | """ 172 | global cw 173 | if lcw in cw: 174 | return str(cw[lcw][0]), cw[lcw][1] 175 | else: 176 | return "wrong_key " + str(lcw), 0 177 | 178 | 179 | def try_read(lcw, _save=True): 180 | """ 181 | try read from bridge, if key not there, save default value 182 | :param lcw: string, local codeword 183 | :param _save: boolean, if not in bridge then save 184 | :return: various 185 | """ 186 | global bridge_client, cw 187 | 188 | temp_cw, default = get_cw_default(lcw) 189 | tmp_str = bridge_client.get(temp_cw) 190 | 191 | if support.is_empty(tmp_str): 192 | tmp = default 193 | if _save: 194 | bridge_client.put(temp_cw, str(tmp)) 195 | else: 196 | if type(default) is int: 197 | try: 198 | tmp = int(tmp_str) 199 | except ValueError: 200 | tmp = default 201 | else: 202 | tmp = tmp_str 203 | return tmp 204 | 205 | 206 | def put(key, value): 207 | """ 208 | Put value to the key in bridge_client 209 | :param key: key 210 | :param value: string 211 | :return: nothing 212 | """ 213 | global bridge_client 214 | # update touch 215 | bridge_client.put("touch", str(time.time())) 216 | bridge_client.put(get_cw(key), str(value)) 217 | 218 | 219 | def get(key): 220 | """ 221 | Get from bridge_client by key, key is expanded through CW 222 | :param key: key 223 | :return: string 224 | """ 225 | global bridge_client 226 | return str(bridge_client.get(get_cw(key))) 227 | 228 | 229 | def export(): 230 | """ 231 | Export bridge_client.json as JSON 232 | :return: JSON string 233 | """ 234 | global bridge_client 235 | return json.dumps(bridge_client.getall()) 236 | 237 | 238 | def get_cmd(): 239 | local_cmd = get("cmd") 240 | if local_cmd is None: 241 | return "" 242 | elif len(local_cmd) > 0: 243 | put("cmd", "") 244 | return local_cmd 245 | -------------------------------------------------------------------------------- /yun-sketch/thermeq3_dht/thermeq3_dht.ino: -------------------------------------------------------------------------------- 1 | //#define DEBUG_PRG 2 | 3 | #include 4 | 5 | //DHT sesnor PIN 6 | #define DHT_PIN A4 7 | 8 | #define TEMP_ERROR_LED 11 9 | #define RELAY_PIN 10 10 | #define STATUS_LED 9 11 | #define ERROR_LED 8 12 | #define LOOP_LED 13 13 | #define BLINK_INTERVAL 150 14 | #define WAIT_UPDATE_SYNC 10000 15 | 16 | //#define IWANNABESAFE 17 | 18 | // define this if you wanna use some optocoupler for relay or reverse logic 19 | #define REVERSE_LOGIC 20 | #ifdef REVERSE_LOGIC 21 | #define LOGIC_1 LOW 22 | #define LOGIC_0 HIGH 23 | #else 24 | #define LOGIC_1 HIGH 25 | #define LOGIC_0 LOW 26 | #endif 27 | 28 | //Install DHTLib http://playground.arduino.cc//Main/DHTLib 29 | //Require DHT22 sensor 30 | #include 31 | 32 | dht DHT; 33 | 34 | //Min DHT22 sampling frequency is 0.5Hz! 35 | #define DHT_INTERVAL 5000 36 | //Bellow these temperature anitfreezing feature is on! 37 | #define MIN_TEMP 7 38 | 39 | unsigned long dhtReadUntil = 0; 40 | 41 | boolean temp_error = false; 42 | boolean temp_anti_freeze_on = false; 43 | boolean temp_led_on = false; 44 | #define TEMP_ERROR_BLINK_PERIOD 500 45 | unsigned long tempBlink = 0; 46 | 47 | 48 | Process p; 49 | char bridgeBuffer[16]; 50 | boolean sysStatus = false; 51 | boolean planned_heating = false; 52 | boolean alarmHit = false; 53 | unsigned long waitUntil = 0; 54 | unsigned long interval = 10 * 1000; // interval in seconds, change 10 to anything you want 55 | unsigned long app_interval = 10 * 60000; // interval in minutes, change 10 to anything you want 56 | unsigned long waitForApp = 0; 57 | 58 | void alloff() { 59 | digitalWrite(LOOP_LED, LOW); 60 | digitalWrite(STATUS_LED, LOW); 61 | digitalWrite(ERROR_LED, LOW); 62 | } 63 | 64 | void allon() { 65 | digitalWrite(LOOP_LED, HIGH); 66 | digitalWrite(STATUS_LED, HIGH); 67 | digitalWrite(ERROR_LED, HIGH); 68 | } 69 | 70 | void blinkLED() { 71 | for (byte i = 0; i < 4; i++) { 72 | allon(); 73 | delay(BLINK_INTERVAL); 74 | alloff(); 75 | delay(BLINK_INTERVAL); 76 | } 77 | } 78 | 79 | void turnIt_nomsg(boolean onoff) { 80 | if (sysStatus == onoff) return; 81 | if (onoff) { 82 | digitalWrite(RELAY_PIN, LOGIC_1); 83 | digitalWrite(STATUS_LED, HIGH); 84 | } else { 85 | if (!alarmHit) 86 | { 87 | digitalWrite(RELAY_PIN, LOGIC_0); 88 | digitalWrite(STATUS_LED, LOW); 89 | } 90 | } 91 | sysStatus = onoff; 92 | } 93 | 94 | void turnIt(boolean onoff) { 95 | showError(false); 96 | if (sysStatus == onoff) return; 97 | if (onoff) { 98 | digitalWrite(RELAY_PIN, LOGIC_1); 99 | digitalWrite(STATUS_LED, HIGH); 100 | } else { 101 | if (!alarmHit) 102 | { 103 | digitalWrite(RELAY_PIN, LOGIC_0); 104 | digitalWrite(STATUS_LED, LOW); 105 | } 106 | } 107 | sysStatus = onoff; 108 | Bridge.put("msg", ""); 109 | } 110 | 111 | void fatalError() { 112 | turnIt(false); 113 | alloff(); 114 | while (true) { 115 | digitalWrite(LOOP_LED, HIGH); 116 | delay(BLINK_INTERVAL); 117 | digitalWrite(LOOP_LED, LOW); 118 | digitalWrite(STATUS_LED, HIGH); 119 | delay(BLINK_INTERVAL); 120 | digitalWrite(STATUS_LED, LOW); 121 | digitalWrite(ERROR_LED, HIGH); 122 | delay(BLINK_INTERVAL); 123 | digitalWrite(ERROR_LED, LOW); 124 | } 125 | } 126 | 127 | void showDead() { 128 | turnIt(false); 129 | alloff(); 130 | while (true) { 131 | for (int j = 0 ; j <= 255; j += 5) { 132 | analogWrite(STATUS_LED, j); 133 | delay(30); 134 | } 135 | for (int j = 255 ; j >= 0; j -= 10) { 136 | analogWrite(STATUS_LED, j); 137 | delay(30); 138 | } 139 | digitalWrite(STATUS_LED, LOW); 140 | delay(BLINK_INTERVAL * 5); 141 | } 142 | } 143 | 144 | void showError(boolean onoff) { 145 | if (onoff) digitalWrite(ERROR_LED, HIGH); 146 | else digitalWrite(ERROR_LED, LOW); 147 | Bridge.put("msg", ""); 148 | } 149 | 150 | void showTempError(boolean onoff) { 151 | if (onoff) { 152 | digitalWrite(TEMP_ERROR_LED, HIGH); 153 | temp_led_on = true; 154 | } 155 | else { 156 | digitalWrite(TEMP_ERROR_LED, LOW); 157 | temp_led_on = false; 158 | } 159 | } 160 | 161 | void tempError(boolean onoff) 162 | { 163 | temp_error = onoff; 164 | } 165 | 166 | void runApp() { 167 | turnIt(false); 168 | // you can use p.begin("/root/nsm.py") without .addParameter 169 | p.begin("python"); 170 | p.addParameter("/root/thermeq3/nsm.py"); 171 | p.runAsynchronously(); 172 | } 173 | 174 | void setup() { 175 | #ifdef DEBUG_PRG 176 | Serial.begin(115200); 177 | while (!Serial); 178 | #endif 179 | 180 | pinMode(RELAY_PIN, OUTPUT); 181 | pinMode(ERROR_LED, OUTPUT); 182 | pinMode(STATUS_LED, OUTPUT); 183 | pinMode(LOOP_LED, OUTPUT); 184 | 185 | blinkLED(); 186 | allon(); 187 | Bridge.begin(); 188 | blinkLED(); 189 | 190 | runApp(); 191 | 192 | #ifdef DEBUG_PRG 193 | Serial.println("GO!"); 194 | Serial.println(interval); 195 | Serial.println(app_interval); 196 | #endif 197 | } 198 | 199 | void loop() { 200 | unsigned long mls = millis(); 201 | 202 | if (mls - waitUntil >= interval) { 203 | digitalWrite(LOOP_LED, HIGH); 204 | Bridge.get("msg", bridgeBuffer, 16); 205 | char c = bridgeBuffer[0]; 206 | 207 | switch (c) { 208 | case 'H': 209 | planned_heating = true; 210 | turnIt(true); 211 | break; 212 | case 'S': 213 | planned_heating = false; 214 | turnIt(false); 215 | break; 216 | case 'E': 217 | #ifdef IWANNABESAFE 218 | planned_heating = false; 219 | turnIt(false); 220 | #endif 221 | showError(true); 222 | break; 223 | case 'A': 224 | alloff(); 225 | break; 226 | case 'Q': 227 | fatalError(); 228 | break; 229 | case 'D': 230 | turnIt(false); 231 | showDead(); 232 | break; 233 | case 'R': 234 | #ifdef IWANNABESAFE 235 | turnIt(false); 236 | #endif 237 | digitalWrite(ERROR_LED, LOW); 238 | digitalWrite(STATUS_LED, LOW); 239 | digitalWrite(ERROR_LED, HIGH); 240 | digitalWrite(STATUS_LED, HIGH); 241 | p.close(); 242 | delay(WAIT_UPDATE_SYNC); 243 | runApp(); 244 | digitalWrite(ERROR_LED, LOW); 245 | digitalWrite(STATUS_LED, LOW); 246 | break; 247 | } 248 | waitUntil = waitUntil + interval; 249 | digitalWrite(LOOP_LED, LOW); 250 | } 251 | 252 | if (mls - dhtReadUntil >= DHT_INTERVAL ) { 253 | dhtReadUntil = dhtReadUntil + DHT_INTERVAL; 254 | int chk = DHT.read22(DHT_PIN); 255 | 256 | switch (chk) 257 | { 258 | case DHTLIB_OK: 259 | tempError(false); 260 | if (DHT.temperature <= MIN_TEMP ) { 261 | alarmHit = true; 262 | turnIt_nomsg(true); 263 | } 264 | else { 265 | alarmHit = false; 266 | if (!planned_heating) { 267 | turnIt_nomsg(false); 268 | } 269 | } 270 | break; 271 | case DHTLIB_ERROR_CHECKSUM: 272 | tempError(true); 273 | alarmHit = true; 274 | turnIt_nomsg(true); 275 | break; 276 | case DHTLIB_ERROR_TIMEOUT: 277 | tempError(true); 278 | alarmHit = true; 279 | turnIt_nomsg(true); 280 | break; 281 | default: 282 | tempError(true); 283 | alarmHit = true; 284 | turnIt_nomsg(true); 285 | break; 286 | } 287 | } 288 | 289 | if (temp_error) 290 | { 291 | if (mls - tempBlink >= TEMP_ERROR_BLINK_PERIOD ) 292 | { 293 | tempBlink = tempBlink + TEMP_ERROR_BLINK_PERIOD; 294 | if (temp_led_on) { 295 | showTempError(false); 296 | } 297 | else { 298 | showTempError(true); 299 | } 300 | } 301 | } 302 | else 303 | { 304 | showTempError(alarmHit); 305 | } 306 | 307 | if (mls - waitForApp >= app_interval) { 308 | #ifdef DEBUG_PRG 309 | Serial.println(mls); 310 | Serial.println(p.running()); 311 | #endif 312 | digitalWrite(LOOP_LED, HIGH); 313 | if (p.running() == false) { 314 | p.close(); 315 | runApp(); 316 | } 317 | waitForApp = waitForApp + app_interval; 318 | digitalWrite(LOOP_LED, LOW); 319 | } 320 | 321 | } 322 | -------------------------------------------------------------------------------- /install/uni_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "thermeq3 install for Yún/RPi" 3 | echo "" 4 | if [ $# -lt 1 ]; then 5 | read -p "Install with default name thermeq3? [y/n]" yn 6 | case $yn in 7 | [Nn]* ) 8 | echo "Usage: uni_install.sh [thermeq3 device name] [alpha]" 9 | exit 1 10 | ;; 11 | esac 12 | DEV_NAME="thermeq3" 13 | else 14 | DEV_NAME=$1 15 | fi 16 | 17 | GITHUB_BASE=https://github.com/autopower/thermeq3/raw/master/install/current/ 18 | if [ "$2" == "alpha" ]; then 19 | read -p "Do you really want upgrade to latest (unstable) alpha? [y/n]" yn 20 | case $yn in 21 | [Yy]* ) 22 | GITHUB_BASE=https://github.com/autopower/thermeq3/raw/master/install/alpha/ 23 | ;; 24 | esac 25 | fi 26 | 27 | PLATFORM=$(uname -m) 28 | case "$PLATFORM" in 29 | *"arm"*) 30 | echo "RPi installer will be used" 31 | PLATFORM="rpi" 32 | BASE_DIR=/home/pi/thermeq3 33 | INSTALL_DIR=/home/pi/thermeq3 34 | WWW_DIR=/var/www/html 35 | ;; 36 | *"mips"*) 37 | echo "Yún installer will be used" 38 | PLATFORM="yun" 39 | BASE_DIR=/root/thermeq3 40 | if [ -d /mnt/sda1 ]; then 41 | INSTALL_DIR=/mnt/sda1 42 | else 43 | if [ -d /mnt/sdb1 ]; then 44 | INSTALL_DIR=/mnt/sdb1 45 | else 46 | echo "Please mount USB or SD card!" 47 | exit 1 48 | fi 49 | fi 50 | WWW_DIR=$INSTALL_DIR/www 51 | ;; 52 | *) 53 | echo "Unknown platform: $PLATFORM" 54 | exit 2 55 | ;; 56 | esac 57 | 58 | echo "Folders:" 59 | echo " - $BASE_DIR as base folder" 60 | echo " - $WWW_DIR as folder for www scripts" 61 | echo " - $INSTALL_DIR as folder for installation files" 62 | 63 | mkdir -p $BASE_DIR 64 | mkdir -p $INSTALL_DIR/install 65 | mkdir -p $BASE_DIR/support 66 | 67 | echo "" 68 | echo "Installing support" 69 | case "$PLATFORM" in 70 | "rpi" ) 71 | echo " - updating apt-get and upgrading packages" 72 | sudo apt-get update 73 | sudo apt-get upgrade 74 | PKG_OK=$(dpkg-query -W --showformat='${Status}\n' python-openssl|grep "install ok installed") 75 | echo Checking for python-openssl: $PKG_OK 76 | if [ "" == "$PKG_OK" ]; then 77 | echo " - no python-openssl. Installing python-openssl" 78 | sudo apt-get --force-yes --yes install python-openssl 79 | if [ $? -ne 0 ]; then 80 | echo "Error during installing openssl library. Error: $?" 81 | fi 82 | exit $? 83 | fi 84 | ;; 85 | "yun" ) 86 | echo " - updating opkg" 87 | opkg update --verbosity=0 88 | if [ $? -ne 0 ]; then 89 | echo "Error during opkg update: $?" 90 | exit $? 91 | fi 92 | echo " - installing unzip" 93 | opkg install unzip --verbosity=0 94 | if [ $? -ne 0 ]; then 95 | echo "Error during installing unzip. Error: $?" 96 | exit $? 97 | fi 98 | echo " - installing openssl libraries" 99 | opkg install python-openssl --verbosity=0 100 | if [ $? -ne 0 ]; then 101 | echo "Error during installing openssl library. Error: $?" 102 | exit $? 103 | fi 104 | ;; 105 | esac 106 | 107 | echo "" 108 | echo "Installing thermeq3" 109 | echo " - downloading thermeq3 app" 110 | wget --no-check-certificate --quiet -O $INSTALL_DIR/install/thermeq3.zip $GITHUB_BASE/thermeq3.zip 111 | if [ $? -ne 0 ]; then 112 | echo "Error during downloading thermeq3 app: $?" 113 | exit $? 114 | fi 115 | 116 | mkdir -p $BASE_DIR/code 117 | echo " - extracting thermeq3 app" 118 | unzip -q -o $INSTALL_DIR/install/thermeq3.zip -d $BASE_DIR/code 119 | if [ $? -ne 0 ]; then 120 | echo "Error during unzipping thermeq3 app: $?" 121 | exit $? 122 | fi 123 | 124 | echo " - creating nsm.py compatibility file" 125 | echo "#!/usr/bin/env python 126 | import sys 127 | sys.path.insert(0, \"$BASE_DIR/code/\") 128 | execfile(\"$BASE_DIR/code/nsm.py\") 129 | " > $BASE_DIR/nsm.py 130 | 131 | case "$PLATFORM" in 132 | "rpi" ) 133 | echo " - creating systemd files" 134 | echo "[Unit] 135 | Description=thermeq3 Service 136 | After=multi-user.target 137 | 138 | [Service] 139 | Type=idle 140 | ExecStart=/usr/bin/python /home/pi/thermeq3/nsm.py 141 | 142 | [Install] 143 | WantedBy=multi-user.target" > /home/pi/thermeq3/install/thermeq3.service 144 | sudo mv /home/pi/thermeq3/install/thermeq3.service /lib/systemd/system/thermeq3.service 145 | sudo chmod 644 /lib/systemd/system/thermeq3.service 146 | sudo systemctl daemon-reload 147 | sudo systemctl enable thermeq3.service 148 | ;; 149 | "yun" ) 150 | echo " - please upload yun sketch via Arduino IDE" 151 | ;; 152 | esac 153 | 154 | echo " - installing shell scripts with $DEV_NAME as device name" 155 | echo "tail -n 50 $INSTALL_DIR/$DEV_NAME.log" > $BASE_DIR/ct 156 | echo "cat $INSTALL_DIR/$1_error.log" > $BASE_DIR/err 157 | echo "ps|grep python" > $BASE_DIR/psg 158 | echo "ps -ef | grep nsm.py | grep -v grep | awk '{print $1}' | xargs kill -9" > $BASE_DIR/killnsm 159 | echo "cat $BASE_DIR/$DEV_NAME.log.* | grep summary | awk '{print $1 "," $8}' | sort > $WWW_DIR/temp.csv 160 | sort -u $WWW_DIR/temp.csv $WWW_DIR/dailysummary.csv > $WWW_DIR/result.csv 161 | rm $WWW_DIR/temp.csv $WWW_DIR/dailysummary.csv 162 | mv $WWW_DIR/result.csv $WWW_DIR/dailysummary.csv" > $BASE_DIR/support/dailysum 163 | chmod +x $BASE_DIR/ct 164 | chmod +x $BASE_DIR/err 165 | chmod +x $BASE_DIR/psg 166 | chmod +x $BASE_DIR/killnsm 167 | chmod +x $BASE_DIR/support/dailysum 168 | 169 | echo " - creating folders:" 170 | case "$PLATFORM" in 171 | "rpi" ) 172 | echo " - for CSV files ($BASE_DIR/csv)" 173 | mkdir -p $BASE_DIR/csv 174 | ;; 175 | "yun" ) 176 | echo " - for CSV files ($INSTALL_DIR/csv)" 177 | mkdir -p $INSTALL_DIR/csv 178 | echo " - for HTML files ($INSTALL_DIR/www)" 179 | mkdir -p $INSTALL_DIR/www 180 | echo " - for CGI-BIN files ($INSTALL/www/cgi-bin)" 181 | mkdir -p $INSTALL_DIR/www/cgi-bin 182 | esac 183 | 184 | 185 | echo " - downloading interactive config" 186 | wget --no-check-certificate --quiet -O $BASE_DIR/config_me.py https://raw.githubusercontent.com/autopower/thermeq3/master/install/current/config_me.py;chmod +x $BASE_DIR/config_me.py 187 | if [ $? -ne 0 ]; then 188 | echo "Error during downloading config app: $?" 189 | exit $? 190 | fi 191 | 192 | echo "" 193 | read -p "Install dashboard? [y/n]" yn 194 | case $yn in 195 | [Yy]*) 196 | echo " - downloading dashboard install script" 197 | case "$PLATFORM" in 198 | "rpi" ) 199 | wget --no-check-certificate --quiet -O $BASE_DIR/install-dash.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/dashboard/rpi-dash.sh;chmod +x $BASE_DIR/install-dash.sh 200 | ;; 201 | "yun" ) 202 | wget --no-check-certificate --quiet -O $BASE_DIR/install-dash.sh https://raw.githubusercontent.com/autopower/thermeq3/master/install/dashboard/yun-dash.sh;chmod +x $BASE_DIR/install-dash.sh 203 | if ! grep -q "0.0.0.0:8180" /etc/config/uhttpd; then 204 | echo "Backing up uhttpd configuration..." 205 | mkdir /root/backup 206 | cp /etc/config/uhttpd /root/backup/uhttpd.old 207 | echo "Modifying uhttpd configuration..." 208 | echo "config uhttpd secondary 209 | list listen_http 0.0.0.0:8180 210 | option home $DIR/www 211 | option cgi_prefix /cgi-bin 212 | option max_requests 2 213 | option script_timeout 10 214 | option network_timeout 10 215 | option tcp_keepalive 1 216 | " >> /etc/config/uhttpd 217 | echo "Restarting uhttpd..." 218 | /etc/init.d/uhttpd restart 219 | fi 220 | ;; 221 | esac 222 | if [ $? -ne 0 ]; then 223 | echo "Error during downloading dashboard install script: $?" 224 | exit $? 225 | fi 226 | echo " - running dashboard install script" 227 | $BASE_DIR/install-dash.sh 228 | ;; 229 | esac 230 | 231 | echo "" 232 | read -p "Install script for proxy support? [y/n]" yn 233 | case $yn in 234 | [Yy]*) 235 | echo "#!/bin/sh 236 | echo "Content-type: text/plain" 237 | echo "" 238 | cat $WWW_DIR/proxy.eq3" > $WWW_DIR/cgi-bin/proxy 239 | esac 240 | 241 | echo "" 242 | read -p "Run interactive config? [y/n]" yn 243 | case $yn in 244 | [Yy]*) 245 | $BASE_DIR/config_me.py 246 | if [ -f $BASE_DIR/location.json ]; then 247 | echo "Moving file..." 248 | case "$PLATFORM" in 249 | "rpi" ) 250 | mv $BASE_DIR/location.json /var/www/html/location.json 251 | ;; 252 | "yun" ) 253 | mv $BASE_DIR/location.json $INSTALL_DIR/www/location.json 254 | ;; 255 | esac 256 | else 257 | echo "Can't find file. Please make location.json file for dashboard!" 258 | fi 259 | esac 260 | 261 | echo "" 262 | read -p "Delete install folder? [y/n]" yn 263 | case $yn in 264 | [Yy]*) 265 | echo "Removing install folder..." 266 | rm -rf $INSTALL_DIR/install/* 267 | rmdir $INSTALL_DIR/install 268 | ;; 269 | esac 270 | 271 | case "$PLATFORM" in 272 | "rpi" ) 273 | echo "Please check apache¾ configuration!" 274 | ;; 275 | "yun" ) 276 | echo "Please upload yun sketch via Arduino IDE!" 277 | ;; 278 | esac 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thermeq3 2 | Boiler actor device for [ELV/EQ-3](http://www.eq-3.de/) [MAX! cube](http://www.eq-3.de/max-heizungssteuerung-produktdetail-kopie/items/bc-lgw-o-tw.html). 3 | Please [take a look](https://github.com/autopower/thermeq3/wiki) at wiki for detailed information. 4 | 5 | ## What's new? 6 | ### 2020-Oct-13 7 | * fixed malformed L response processing, max 128 devices can be processed (more that EQ3 allows) 8 | 9 | ### 2020-Oct-07 10 | * fixed wrong processing of eq3 cube data, especially L response. If cube is somehow misconfigured, e.g. device is present in L response, but there is no device in C response, loop never ends. 11 | 12 | ### 2020-Sep-29 13 | * alpha V307, added support for svpnmw in profiles 14 | 15 | ### 2020-May-24 16 | * alpha V304, add fix for manual mode to check if temperature is over setup.summertime, and if its over, do not run heating, just quick fix to handle inconsistent MaxCube behaviour in summer 17 | 18 | ### 2020-Apr-04 19 | * alpha V302, with more diagnostics for temp/time profiles 20 | 21 | ### 2020-Jan-07 22 | * alpha V299, manual mode fully implemented, thermeq3 is now completely fail safe from Max!Cube errors, such as lost configuration etc. 23 | * open windows adjusment fixed 24 | 25 | ### 2019-Dec-31 26 | * alpha V298, many V297 bugs fixed :) 27 | * known issue: open window adjustment can go wrong, debugging support for maxeq3 module added 28 | 29 | ### 2019-Dec-29 30 | * alpha V297 31 | * proxy fully implemented 32 | * multiple checks and error implemented, especially for wrong M response from MaxCube 33 | * manual mode implemented as fail safe, if any error experienced, thermeq3 switched to manual mode to retain heating 34 | * minor improvements 35 | 36 | ### 2019-Nov-17 37 | * multiple checks and error response added in alpha V295 38 | * support for proxying Max!Cube response to other devices, new file proxy.eq3 generated every cube read 39 | 40 | ### 2019-Nov-15 41 | * alpha V294 ready for standalone operation, e.g. no need for yun or RPi, can operate in any linux/windows distro 42 | * relay actions separated into extaction.py file, this must be implemented to suite your separate relay 43 | 44 | ### 2019-Feb-16 45 | * config_me script updated 46 | * dashboard updated 47 | * alpha V287 with new yahoo weather support 48 | 49 | ### 2019-Feb-15 50 | If you experiencing problems with retired yahoo weather code, please switch to OpenWeather. Simply update bridge value `weather_reference` with value `owm`, e.g. http:///data/put/weather_reference/owm 51 | If you want still use yahoo weather, please get your AppID and keys on [this page](https://developer.yahoo.com/weather/). Support for new API will be available in few days. 52 | 53 | ### 2019-Feb-01 54 | Yahoo weather API is [retired](https://developer.yahoo.com/weather/?guccounter=1). Please wait for fix. 55 | 56 | ### 2018-Nov-25 57 | * minor fix 58 | * Python 3 compatible version in alpha 59 | 60 | ### 2018-Nov-19 61 | * RPi port update, minor fixes 62 | * new [universal installer](https://github.com/autopower/thermeq3/blob/master/install/uni_install.sh) for both platforms, feel free to test 63 | * redesigned dashboard installers 64 | 65 | ### 2018-Nov-13 66 | * RPi port in alpha stage, testers still wanted 67 | 68 | ### 2018-Nov-11 69 | * RPi support added, testers wanted 70 | 71 | ### 2018-Nov-09 72 | * simple external device based on ESP8266 and eInk to display status with open windows list. Feel free to test and comment. 73 | 74 | ### 2018-Oct-22 75 | * calling to arms Yún Rev2 users, I'll be pointed to some incompatibilities between Rev1 and Rev2. Rev2 users, please use `https://github.com/autopower/thermeq3/tree/master/install/alpha_Yun_Rev2` 76 | install script to check if install script/wget is working flawless 77 | 78 | ### 2018-Jul-06 79 | * someone calls for manual heating override, please take a look at new dashboard. For those who sports simple URL, please use `http://ardu.ip/data/put/msg/S` to stop or 80 | `http://ardu.ip/data/put/msg/H` to start heating 81 | 82 | ### 2017-Oct-14 83 | * possibility to get temperature and humidity from local source, bridge values: local_temp, local_humidity and weather_reference 84 | * fixed persistence of values after reboot 85 | * complete system dump in bridge, key "dump" 86 | 87 | ### 2017-Oct-10 88 | * minor fixes in nsm and dashboard 89 | * separated mail user for login and from address variable 90 | 91 | ### 2017-Oct-07 92 | * readme update 93 | * yun sketch update addressing wrong response from cube (typically wrong M response) 94 | * watchdog script preparedness 95 | 96 | ### 2017-Sep-16 97 | * V250 in alpha 98 | * weather fixes 99 | * new config file support 100 | * lifetime valve ignore from config file, there is no need to add valve manual via bridge! 101 | 102 | ### 2017-Jan-14 103 | * file del_dev.py in support directory for deleting devices from cube. In case that you have corrupted condfiguration and many Wrong address errors in log file. 104 | 105 | ### 2016-Dec-30 106 | * for the bravest there is an [alpha channel](https://github.com/autopower/thermeq3/tree/master/install/alpha) 107 | * this is only for upgrade, please use [this upgrade script](https://github.com/autopower/thermeq3/tree/master/install/alpha/upgrade.sh) 108 | 109 | ### 2016-Dec-25 110 | * bridge file moved to sd card, if found in `/root` then moved do sd card 111 | * some new shell scripts below 112 | * [upgrade](https://github.com/autopower/thermeq3/tree/master/install/upgrade.sh) thermeq3 113 | * [recreate uhttpd files](https://github.com/autopower/thermeq3/tree/master/install/recreate_http.sh) on sd card 114 | * [create nsm file](https://github.com/autopower/thermeq3/tree/master/install/create_nsm.sh) in `/root`, just for compatibility issues 115 | * wall thermostat temperature fixed 116 | 117 | ### 2016-Nov-13 118 | * RPi preparedness. Please follow instruction in code. Unfortunatelly I don't have RPi to test it, please report if you need any changes. 119 | 120 | ### 2016-Nov-01 121 | * Lot of fixes. 122 | 123 | ### 2016-Apr-08 124 | * to use "current" temperature for loggging find out this text `# comment line below to use current temp` in nsm.py (for V1xx) or in thermeq3.py (for V2xx) and follow 125 | instructions. Please keep in mind, that current temperature on heater thermostat can be 0.0 (zero) because of limitations of HT! 126 | 127 | ### 2016-Mar-12 128 | * despite fact that V1xx is obsolete, here is the new version, V152, fixing some bugs, thanks to TonyV from UK :0 129 | 130 | ### 2015-Dec-18 131 | * lib version updated 132 | 133 | ### 2015-Dec-13 134 | * directory structure reorganised 135 | * new install script for all version (old one in one file (nsm.py), new one with libraries) 136 | 137 | ### 2015-Dec-11 138 | * some cleanup on github 139 | 140 | ### 2015-Dec-10 141 | * new librarysed version in betabeat 142 | * new install script for lib version in scripts 143 | * some code overhaul :) 144 | 145 | ### 2015-Dec-04 146 | * betabeat: new RPi version 147 | 148 | ### 2015-Nov-28 149 | * betabeat: bridge routine rewrited, some support for literal processing 150 | * betabeat: ignore intervals corrected 151 | * betabeat: you can ignore valve forever, just edit bridgefile and to ign codeword add valveserial and time since epoch=1924991999, its 31/Dec/2030 :), e.g. {"06ABCD": 1924991999} 152 | * betabeat: new boiler controlling variable, svpnmw (Single Valve Position, No Matter What) in %, if any of valves opened more than svpnmw then heating is on, to turn off just set to 101% 153 | 154 | ### 2015-Nov-27 155 | * betabeat: new weather API used, yahoo YQL and OWM, API key for OWM is from OWM example page, please change it 156 | * just for sure, use "new" bridge python library from https://codeload.github.com/arduino/YunBridge/zip/master 157 | 158 | ### 2015-Nov-26 159 | * new codeword, mode, can be auto or manual, added after request, in auto mode thermeq3 sends H/S commands to arduino part, in manual mode do nothing :) 160 | 161 | ### 2015-Nov-02 162 | * betabeat: some javascript code (based on jquery ui) to control device, ugly, not fully functional, first steps with JS and CSS ;) 163 | * betabeat: code cleanup 164 | 165 | ### 2015-Nov-01 166 | * betabeat: new arduino command A, clears LED 167 | * betabeat: some cleanup, codeword dump removed 168 | 169 | ### 2015-Oct-31 170 | * minor fixes 171 | * new status messages 172 | * JSON formated string in current status 173 | 174 | ### 2015-Oct-21 175 | * some fix in RPi version, please check commented code and uncomment (setblocking) 176 | * if anyone need room names in CSV, uncomment code in exportCSV() 177 | * minor updates 178 | 179 | ### 2015-Oct-10 180 | * minor updates, betabeat and production are the same version 181 | * alpha RPi version, only change is the abstraction code replacing bridgeclient (but as reported from users, it works! :)) 182 | 183 | ### 2015-Mar-23 184 | * minor updates 185 | 186 | ### 2015-Mar-15 187 | * updating valve ignore interval according to outer temperature 188 | 189 | ### 2015-Jan-18 190 | * more pythonic code, classes etc. 191 | 192 | ### 2015-Jan-07 193 | * betabeat: yahoo weather and oww interval sampling working 194 | * betabeat: in secondary web folder `nice.html` file is generated, contains nice formated status (hm, if pre means nice) 195 | 196 | ### 2015-Jan-06 197 | * support for yahoo weather, stay tuned for open window interval auto update by actual weather situation (temperature, humidity), better ventilation 'support' 198 | * resample function 199 | * support directory in betabeat, check code for sampling function, modify to your needs 200 | 201 | ### 2015-Jan-03 202 | * betabeat: if webserver directory not exist, is created 203 | * betabeat: correct response on secondary web server, eg: http://arduino.ip:second_port/cgi-bin/status returns app/json from status.xml file 204 | 205 | ### 2014-Dec-28 206 | * betabeat: open window list and current status separated into secondary web server as xml files, accessible w/o any credentials 207 | 208 | ### 2014-Dec-27 209 | * Simple install script: update opkg, installs openssl for python, create some scripts for nsm.py controlling. 210 | 211 | ### 2014-Dec-26 212 | * Ignoring valve after closing window 213 | * Redesigned rooms/valves listing in log file 214 | * Support for tasker, new bridge values: openwinlist and current_status, please take a look at `tasker` directory, there's simple example how to list open windows. Usefull especially when leaving house :) 215 | * New CSV file handling, file is generated daily. New column in CSV, after date/time col, you can read 0/1 for heating (0=off, 1=on), so you can analyze when is boiler turned on or off (no more cat log | grep) 216 | * Redesigned bridge functions (load/save), little bit failsafe (nothing extraordinary) 217 | * Implemented support for day table, just enable beta functionality (thermeq3.ip/data/put/beta/yes) and edit table in nsm.py. You can control boiler in different way during day. 218 | 219 | # Thats all folks. Stay tuned :) 220 | -------------------------------------------------------------------------------- /obsolete/lib/t3_var.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import profiles 4 | import os 5 | import errno 6 | import bridge 7 | import support 8 | import config 9 | 10 | 11 | class Thermeq3Status(object): 12 | def __init__(self): 13 | self.status_str = { 14 | "i": "idle", 15 | "h": "heating", 16 | "s": "starting", 17 | "d": "dead", 18 | "hv": "heating and ventilating", 19 | "iv": "idle and ventilating", 20 | "m": "manual"} 21 | self.actual = '' 22 | 23 | def update(self, status_key): 24 | """ 25 | Update status string 26 | :param status_key: string, key in status_str 27 | :return: nothing 28 | """ 29 | if status_key not in self.status_str: 30 | key_error_str = "Key error" 31 | bridge.put("status", key_error_str) 32 | self.actual = key_error_str 33 | else: 34 | bridge.put("status", self.status_str[status_key]) 35 | bridge.put("status_key", status_key) 36 | self.actual = self.status_str[status_key] 37 | 38 | 39 | class Thermeq3Setup(object): 40 | def __init__(self): 41 | # thermeq3 configuration variables, override in /root/config.py 42 | self.version = 253 43 | # window ignore time, in minutes 44 | self.window_ignore_time = 15 45 | # my IP address 46 | self.my_ip = "127.0.0.1" 47 | # sd card or usb key mount point, default is /mnt/sda1/ 48 | self.place = "" 49 | # where is stderr log located, default setup.place + setup.device_name + "_error.log" 50 | self.stderr_log = "" 51 | # difference from last known valve value, in % 52 | self.percentage = 3 53 | # abnormal count of warning is 54 | self.abnormalCount = 30 55 | # how many windows must be open to recognize that we are ventilating 56 | self.ventilate_num = 3 57 | self.timeout = 10 58 | self.ext_port = 80 59 | # which mode is selected: [TIME, TEMP, NORMAL] 60 | self.selected_mode = "TIME" 61 | # control values 62 | self.preference = "per" 63 | self.valve_switch = 35 64 | # single valve position, no matter what 65 | self.svpnmw = 80 66 | self.total_switch = 150 67 | self.valve_num = 2 68 | self.au = True 69 | self.ignore_time = 30 70 | self.no_oww = False 71 | self.log_filename = "" 72 | self.csv_log = "" 73 | self.bridge_file = "" 74 | self.intervals = {} 75 | self.temp = [] 76 | self.day = [] 77 | # hard coded ignored valves 78 | self.hard_ignored = {} 79 | # which values will be written into csv file (1 = actual temp, 2 = set temp, 3 = both) 80 | self.csv_values = 1 81 | 82 | # Required per-install variables, configured in /root/config.py 83 | # Reported name of this thermeq3 installation ie. "thermeq3" 84 | self.device_name = "thermeq3" 85 | # MAX Cube 86 | # ie. "192.168.0.10" 87 | self.max_ip = None 88 | # e-mail 89 | # ie. "devices@foo.local" 90 | self.from_addr = None 91 | # ie. "user@foo.local", or a list: ["user1@foo.local", user@bar.local] 92 | self.to_addr = None 93 | # SMTP host ie. "mail.foo.local" 94 | self.mail_server = None 95 | # SMTP port ie. 25 96 | self.mail_port = None 97 | # SMTP authentication password, ie. "password" 98 | self.from_pwd = None 99 | # Weather info 100 | # open weather map API key, ie "123456789" 101 | self.owm_api_key = None 102 | # geographic location, as per Yahoo WOEID. ie. "12345" 103 | self.yahoo_location = None 104 | self.owm_location = None 105 | 106 | self.err_str = "" 107 | 108 | support.guess_platform() 109 | # import /root/config.py, overriding per-install variables above 110 | if support.is_win(): 111 | # execfile("t:/root/config.py") 112 | old = "t:/root/config.py" 113 | new = "t:/root/thermeq3.json" 114 | else: 115 | # execfile("/root/config.py") 116 | old = "/root/config.py" 117 | new = "/root/thermeq3.json" 118 | 119 | result = config.load_old(old, new) 120 | if result == 1: 121 | self.err_str = "Info: can't find old config file.\n" 122 | elif result == 2: 123 | self.err_str = "Info: error processing old config file.\n" 124 | elif result == 3: 125 | self.err_str = "Info: you can't see something like this.\n" 126 | elif result == 4: 127 | self.err_str = "Info: error processing new config file.\n" 128 | elif result == 0: 129 | if support.is_win(): 130 | cmd = "ren " + old + " config.old" 131 | os.system(cmd.replace("/", "\\")) 132 | else: 133 | os.system("mv " + old + " /root/config.old") 134 | 135 | # seems everything is OK, so load config 136 | result = config.load(new) 137 | if result == {}: 138 | self.err_str += "Error: can't find or load config file!\nPlease run config_me.py" 139 | sys.exit(self.err_str) 140 | else: 141 | try: 142 | obj = eval("self") 143 | except SyntaxError: 144 | self.err_str += "Error while evaluating object!" 145 | sys.exit(self.err_str) 146 | else: 147 | for k, v in result.iteritems(): 148 | try: 149 | if str(v).isdigit(): 150 | vs = int(v) 151 | else: 152 | vs = str(v) 153 | setattr(obj, str(k), vs) 154 | except NameError: 155 | pass 156 | 157 | # check if SD or USB is mounted, if not raise error 158 | if not self.init_paths(): 159 | self.err_str = "Error: can't find mounted storage device!\n" + \ 160 | "Please mount SD card or USB key and run program again.\n" 161 | 162 | # create www directory, if now www directory exist raise error 163 | try: 164 | os.makedirs(self.place + "www") 165 | except OSError as exception: 166 | if exception.errno != errno.EEXIST: 167 | self.err_str = "Error: can't create www directory!\nPlease check mounted storage device." 168 | raise 169 | 170 | def __repr__(self): 171 | return str(self.__class__) + ": " + str(self.__dict__) 172 | 173 | def __str__(self): 174 | return str(self.__class__) + ": " + str(self.__dict__) 175 | 176 | def init_paths(self): 177 | if support.is_win(): 178 | if os.path.exists("t:/mnt/sda1"): 179 | self.place = "t:/mnt/sda1/" 180 | elif os.path.exists("t:/mnt/sdb1"): 181 | self.place = "t:/mnt/sdb1/" 182 | else: 183 | return False 184 | # init path variables 185 | self.log_filename = self.place + self.device_name + ".log" 186 | self.csv_log = self.place + "csv/" + self.device_name + ".csv" 187 | self.bridge_file = self.place + self.device_name + ".bridge" 188 | self.stderr_log = self.place + self.device_name + "_error.log" 189 | return True 190 | elif support.is_yun(): 191 | if os.path.ismount("/mnt/sda1"): 192 | self.place = "/mnt/sda1/" 193 | elif os.path.ismount("/mnt/sdb1"): 194 | self.place = "/mnt/sdb1/" 195 | else: 196 | return False 197 | # init path variables 198 | self.log_filename = self.place + self.device_name + ".log" 199 | self.csv_log = self.place + "csv/" + self.device_name + ".csv" 200 | self.bridge_file = self.place + self.device_name + ".bridge" 201 | self.stderr_log = self.place + self.device_name + "_error.log" 202 | return True 203 | elif support.is_rpi(): 204 | # TBI RPI 205 | # please update path according to RPi environment 206 | if os.path.ismount("/mnt/sda1"): 207 | self.place = "/mnt/sda1/" 208 | elif os.path.ismount("/mnt/sdb1"): 209 | self.place = "/mnt/sdb1/" 210 | else: 211 | return False 212 | # init path variables 213 | self.log_filename = self.place + self.device_name + ".log" 214 | self.csv_log = self.place + "csv/" + self.device_name + ".csv" 215 | self.bridge_file = self.place + self.device_name + ".bridge" 216 | self.stderr_log = self.place + self.device_name + "_error.log" 217 | return True 218 | 219 | def init_intervals(self): 220 | # threshold in seconds, so 10 minutes are 10*60 seconds 221 | # interval as "name": [interval=how often check, mute int, next_time] 222 | tm = time.time() 223 | self.intervals = { 224 | "max": [120, 0, tm], 225 | "upg": [4 * 60 * 60, 0, tm], 226 | "var": [10 * 60, 0, tm], 227 | # threshold, send every X, muted for X 228 | "oww": [10 * 60, 30 * 60, 60 * 60], 229 | # threshold, muted for X, time.time() 230 | "wrn": [6 * 60 * 60, 24 * 60 * 60, tm], 231 | "err": [0, 0, 0.0], 232 | # sleep value 233 | "slp": [30, 3, tm]} 234 | # day windows/intervals 235 | # day = [0/from_str, 1/to_str, 2/total or per, 3/mode ("total"/"per"), 4/check interval, 5/valves] 236 | self.day = [ 237 | ["00:00", "06:00", 34, "per", 180, 2], 238 | ["06:00", "10:00", 36, "per", 90, 2], 239 | ["10:00", "16:00", 38, "per", 120, 2], 240 | ["16:00", "22:00", 36, "per", 90, 2], 241 | ["22:00", "23:59", 34, "per", 120, 2]] 242 | # temperature table 243 | self.temp = [ 244 | [-30, -20, 20, "per", 90, 2], 245 | [-20, -10, 25, "per", 120, 2], 246 | [-10, 0, 28, "per", 120, 2], 247 | [0, 10, 30, "per", 180, 2], 248 | [10, 20, 40, "per", 240, 2], 249 | ] 250 | profiles.init(self.day, self.temp) 251 | 252 | 253 | class Thermeq3Variables(object): 254 | def __init__(self): 255 | self.appStartTime = time.time() 256 | # heat times; total: [totalheattime, time.time()] 257 | self.ht = {} 258 | # device log, used to count if valve position didn't change 259 | self.dev_log = {} 260 | # message queue 261 | self.msgQ = [] 262 | # index in mode table 263 | self.act_mode_idx = -1 264 | # variable for weather situation 265 | self.situation = {} 266 | # CSV file 267 | self.csv = None 268 | # number of readings when we heating 269 | self.heat_readings = 0 270 | # heating is off 271 | self.heating = False 272 | # and we are not ventilating 273 | self.ventilating = False 274 | 275 | def __repr__(self): 276 | return str(self.__class__) + ": " + str(self.__dict__) 277 | 278 | def __str__(self): 279 | return str(self.__class__) + ": " + str(self.__dict__) 280 | -------------------------------------------------------------------------------- /install/current/config_me.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import platform 3 | import subprocess 4 | import os 5 | import json 6 | import urllib 7 | import urllib2 8 | import traceback 9 | import uuid 10 | import hmac 11 | import hashlib 12 | import time 13 | from base64 import b64encode 14 | 15 | 16 | run_target = "" 17 | cfg = {} 18 | 19 | 20 | # noinspection PyBroadException 21 | def get_yahoo_data(woe_id, yahoo_data): 22 | city = "Error" 23 | if woe_id is None: 24 | city = "WOEID None" 25 | else: 26 | # basic info 27 | url = 'https://weather-ydn-yql.media.yahoo.com/forecastrss' 28 | method = 'GET' 29 | try: 30 | data = json.loads(yahoo_data) 31 | except Exception: 32 | pass 33 | else: 34 | app_id = str(data["app_id"]) 35 | consumer_key = str(data["consumer_key"]) 36 | consumer_secret = str(data["consumer_secret"]) 37 | concat = '&' 38 | query = {'woeid': str(woe_id), 'format': 'json', 'u': 'c'} 39 | oauth = { 40 | 'oauth_consumer_key': consumer_key, 41 | 'oauth_nonce': uuid.uuid4().hex, 42 | 'oauth_signature_method': 'HMAC-SHA1', 43 | 'oauth_timestamp': str(int(time.time())), 44 | 'oauth_version': '1.0' 45 | } 46 | 47 | # Prepare signature string (merge all params and SORT them) 48 | merged_params = query.copy() 49 | merged_params.update(oauth) 50 | sorted_params = [k + '=' + urllib.quote(merged_params[k], safe='') for k in sorted(merged_params.keys())] 51 | signature_base_str = method + concat + urllib.quote(url, safe='') + concat + urllib.quote( 52 | concat.join(sorted_params), safe='') 53 | 54 | # Generate signature 55 | composite_key = urllib.quote(consumer_secret, safe='') + concat 56 | oauth_signature = b64encode(hmac.new(composite_key, signature_base_str, hashlib.sha1).digest()) 57 | 58 | # Prepare Authorization header 59 | oauth['oauth_signature'] = oauth_signature 60 | auth_header = 'OAuth ' + ', '.join(['{}="{}"'.format(k, v) for k, v in oauth.iteritems()]) 61 | 62 | # Send request 63 | url = url + '?' + urllib.urlencode(query) 64 | request = urllib2.Request(url) 65 | request.add_header('Authorization', auth_header) 66 | request.add_header('Yahoo-App-Id', app_id) 67 | try: 68 | data = urllib2.urlopen(request).read() 69 | except Exception, error: 70 | pass 71 | else: 72 | if data is not None: 73 | data = json.loads(data) 74 | try: 75 | city = data["location"]["city"] 76 | except Exception: 77 | pass 78 | 79 | return city 80 | 81 | 82 | def get_owm_data(city, owm_api_key): 83 | owm_id = None 84 | 85 | if owm_api_key is None: 86 | print("OWM API key not set!") 87 | else: 88 | # and check if yahoo is correct 89 | url = "http://api.openweathermap.org/data/2.5/weather?q=" + str(city) + "&appid=" + owm_api_key + \ 90 | "&units=metric" 91 | try: 92 | result = json.load(urllib2.urlopen(url)) 93 | except Exception, error: 94 | print("OWM communication error: " + str(error)) 95 | print("Traceback: " + str(traceback.format_exc())) 96 | owm_id = -1 97 | else: 98 | if "main" in result and "id" in result: 99 | owm_id = result["id"] 100 | else: 101 | print("Error during parsing result.") 102 | 103 | return str(owm_id) 104 | 105 | 106 | def guess_platform(): 107 | """ 108 | Returns platform in global variable 109 | """ 110 | global run_target 111 | gos = str(platform.platform()).upper() 112 | target = "rpi" 113 | if "WINDOWS" in gos: 114 | target = "win" 115 | elif "LINUX" in gos: 116 | gm = str(platform.machine()).upper() 117 | if "MIPS" in gm: 118 | target = "yun" 119 | elif "ARM" in gm: 120 | target = "rpi" 121 | run_target = target 122 | 123 | 124 | def ping(host): 125 | """ 126 | Returns True if host responds to a ping request 127 | """ 128 | # Ping parameters as function of OS 129 | ping_str = "-n 1" if platform.system().lower() == "windows" else "-c 1" 130 | args = "ping " + " " + ping_str + " " + host 131 | need_sh = False if platform.system().lower() == "windows" else True 132 | # Ping 133 | return subprocess.call(args, shell=need_sh) == 0 134 | 135 | 136 | def load(config_file): 137 | f = None 138 | result = {} 139 | try: 140 | f = open(config_file, "r") 141 | except IOError: 142 | pass 143 | else: 144 | if f is not None: 145 | result = json.load(f) 146 | finally: 147 | if f is not None: 148 | f.close() 149 | return result 150 | 151 | 152 | def save(config_file): 153 | global cfg 154 | 155 | write_to_file(config_file, cfg) 156 | 157 | 158 | def load_yahoo(): 159 | global cfg 160 | if "yahoo" in cfg: 161 | tmp = json.loads(cfg["yahoo"]) 162 | cfg.update({"app_id": tmp["app_id"]}) 163 | cfg.update({"consumer_key": tmp["consumer_key"]}) 164 | cfg.update({"consumer_secret": tmp["consumer_secret"]}) 165 | 166 | 167 | def write_to_file(file_name, file_payload): 168 | try: 169 | f = open(file_name, "w") 170 | except IOError: 171 | print("Error during writing file: " + file_name) 172 | else: 173 | json.dump(file_payload, f) 174 | f.close() 175 | 176 | 177 | def load_old(old_config_file, new_config_file): 178 | cf_1 = ["self.devname", "self.max_ip", "self.fromaddr", "self.toaddr", "self.mailserver", "self.mailport", 179 | "self.frompwd", "self.owm_api_key", "self.location", "self.extport"] 180 | cf_2 = ["self.device_name", "self.max_ip", "self.from_addr", "self.to_addr", "self.mail_server", "self.mail_port", 181 | "self.from_pwd", "self.owm_api_key", "self.yahoo_location", "self.ext_port"] 182 | ncf = {} 183 | if os.path.exists(old_config_file): 184 | try: 185 | f = open(old_config_file, "r") 186 | except IOError: 187 | print("IOError during opening olf config file: " + old_config_file) 188 | else: 189 | num = 0 190 | for line in f: 191 | is_comment = line.find('#') 192 | if is_comment == 0: 193 | pass 194 | else: 195 | if is_comment > 0: 196 | line = line[:-is_comment] 197 | w = line.split("=") 198 | wr = w[0].rstrip() 199 | if not w[0] == "\n": 200 | if "toaddr" in wr: 201 | wv = w[1].lstrip().rstrip() 202 | else: 203 | wv = w[1].lstrip().rstrip().replace('"', '') 204 | if wr in cf_1: 205 | num += 1 206 | idx = cf_1.index(wr) 207 | ncf.update({cf_2[idx].replace("self.", ""): str(wv)}) 208 | if num < len(cf_1): 209 | # some commands are missing 210 | print("Old config file is incomplete, some command missing!") 211 | else: 212 | # everything is ok 213 | print("New config file created, now saving...") 214 | f.close() 215 | write_to_file(new_config_file, ncf) 216 | 217 | 218 | def get_config(rew): 219 | global cfg 220 | 221 | input_string = [["max_ip", "IP address of Max! Cube"], 222 | ["from_addr", "sender address [From]"], 223 | ["to_addr", "recipient address [To]"], 224 | ["mail_server", "mail server address"], 225 | ["mail_port", "mail server port", 25], 226 | ["from_user", "mail server user"], 227 | ["from_pwd", "mail server password"], 228 | ["device_name", "device name", "thermeq3"], 229 | ["ext_port", "external port", 29080], 230 | ["owm_api_key", "open weather API key"], 231 | ["yahoo_location", "Yahoo location ID", 823123], 232 | ["app_id", "Yahoo AppID"], 233 | ["consumer_key", "Yahoo consumer key"], 234 | ["consumer_secret", "Yahoo consumer secret"], 235 | ["csv_values", ":\n1 if only set temp\n2 if only actual temp\n3 both temp\nis written into csv", 1], 236 | ["hard_ignored", "valve ID to ignore forever (q to quit)"]] 237 | 238 | config_str = {} 239 | ignored = {} 240 | for k in input_string: 241 | if k[0] == "hard_ignored": 242 | value = "" 243 | while not value == "q": 244 | value = raw_input("Please enter " + k[1] + ": ") 245 | if value == "q": 246 | pass 247 | else: 248 | ignored.update({str(value): 1924991999}) 249 | value = json.dumps(ignored) 250 | else: 251 | txt = " " 252 | if len(k) > 2 and rew is True: 253 | txt += k[1] + " (default: " + str(k[2]) + ")" 254 | elif k[0] in cfg: 255 | txt += k[1] + " (default: " + str(cfg[k[0]]) + ")" 256 | else: 257 | txt += k[1] 258 | value = raw_input("Please enter" + txt + ": ") 259 | if value == "" and (len(k) > 2 or k[0] in cfg): 260 | if len(k) > 2 and rew is True: 261 | value = k[2] 262 | elif k[0] in cfg: 263 | value = cfg[k[0]] 264 | print("\tUsing defaults (" + str(value) + ").") 265 | if k[0] == "max_ip": 266 | if ping(str(value)) is False: 267 | print("Cannot ping host " + str(value) + "!") 268 | config_str.update({k[0]: str(value)}) 269 | 270 | tmp_yahoo = {"app_id": config_str["app_id"], 271 | "consumer_key": config_str["consumer_key"], 272 | "consumer_secret": config_str["consumer_secret"]} 273 | config_str.update({"yahoo": json.dumps(tmp_yahoo)}) 274 | del config_str["app_id"] 275 | del config_str["consumer_key"] 276 | del config_str["consumer_secret"] 277 | 278 | print("Final config string:\n" + json.dumps(config_str)) 279 | cfg = config_str 280 | 281 | 282 | def do_config(file_name, rewrite): 283 | get_config(rewrite) 284 | save(file_name) 285 | print("Config file saved into " + file_name) 286 | 287 | 288 | if __name__ == '__main__': 289 | print("thermeq3 interactive config\n") 290 | guess_platform() 291 | print("Platform is " + str(run_target)) 292 | if run_target == "win": 293 | old = "t:/root/config.py" 294 | new = "t:/root/thermeq3.json" 295 | else: 296 | old = "/root/config.py" 297 | new = "/root/thermeq3.json" 298 | # just for testing 299 | print("Using " + old + " as old and " + new + " as new config file") 300 | if os.path.exists(new) and "test" in new: 301 | print("It looks like you testing config. Deleting file " + new) 302 | os.remove(new) 303 | # testing end 304 | 305 | if os.path.exists(old): 306 | print("Old config file found.") 307 | load_old(old, new) 308 | if run_target == "win": 309 | cmd = "ren " + old + " config.old" 310 | os.system(cmd.replace("/", "\\")) 311 | else: 312 | os.system("mv " + old + " /root/config.old") 313 | elif os.path.exists(new) is False: 314 | # if nothing exist in old config file and there is no new config file, get config 315 | print("There is no new config file!") 316 | do_config(new, True) 317 | save(new) 318 | else: 319 | print("New config file " + new + " found.") 320 | ret_value = raw_input("Replace config file [N/y]:").upper() 321 | if ret_value == "" or ret_value == "N": 322 | print("You choose keep current config file.") 323 | elif ret_value == "Y": 324 | cfg = load(new) 325 | load_yahoo() 326 | print("Backuping old config file...") 327 | if run_target == "win": 328 | cmd = "ren " + new + " thermeq3.bak" 329 | os.system(cmd.replace("/", "\\")) 330 | else: 331 | os.system("mv " + new + " /root/thermeq3.jsonbackup") 332 | do_config(new, False) 333 | save(new) 334 | 335 | # check weather location 336 | print("Loading config file to check weather:") 337 | cfg = load(new) 338 | if "yahoo_location" in cfg: 339 | # ret_value = check_for_locations(cfg["yahoo_location"], cfg["owm_api_key"]) 340 | ret_value = get_yahoo_data(cfg["yahoo_location"], cfg["yahoo"]) 341 | ret_value = get_owm_data(ret_value, cfg["owm_api_key"]) 342 | if ret_value is not None: 343 | # and update owm location 344 | if "owm_location" in cfg: 345 | print("OWM location updated in config.") 346 | else: 347 | print("OWM location added to config.") 348 | cfg.update({"owm_location": ret_value}) 349 | save(new) 350 | print("Config file saved into " + new) 351 | 352 | # prepare location.json file 353 | tmp_location = {"yahoo_location": cfg["yahoo_location"], "yahoo": cfg["yahoo"]} 354 | tmp_path = os.path.dirname(new) + "/location.json" 355 | write_to_file(tmp_path, tmp_location) 356 | -------------------------------------------------------------------------------- /obsolete/betabeat/html/setup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | thermeq 6 | 7 | 8 | 9 | 10 | 315 | 355 | 356 | 357 | 358 | 359 |

360 | 363 | 379 | 380 |
381 |
382 | 383 | 384 | 385 | 386 |
387 | 388 |
389 | 390 | 391 | 392 | 393 | 394 | 395 |
396 | 397 |
398 |
399 | 400 | 401 |
402 |
403 |
404 | 405 |
406 |
407 | 408 |
409 |
410 | 411 | 412 |
413 |
414 |
415 | 416 |
417 |
418 | 419 | 420 |
421 |
422 |
423 | 424 |
425 |
426 | 427 |
428 |
429 | 430 | 431 |
432 |
433 |
434 | 435 |
436 |
437 | 438 |
439 |
440 |
441 | 442 | 443 | 444 |
445 |
446 |
447 | 448 |
449 |
450 |
451 | 452 | 453 | 454 |
455 |
456 |
457 | 458 |
459 |
460 | 461 | 462 | 463 |
464 |
465 | 466 |
467 |
468 | 469 | 470 |
471 |
472 |
473 | 474 |
475 |
476 |
477 |
478 | 479 | 482 | 483 |
484 | 485 | 486 | 487 | 488 | 489 | 490 | -------------------------------------------------------------------------------- /eInk Display/sketch/exthermeq.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "FS.h" 15 | 16 | #define DEBUG_PRG // use if need debug via serial 17 | #define COLORED 1 // value for eInk 18 | #define UNCOLORED 0 // value for eInk 19 | #define MAX_VAL 32 // max array size for valves and openwindows 20 | #define SSID_NAME "Your_SSID" // SSID name 21 | #define SSID_PWD "Your_key" // SSID pwd 22 | #define MAX_VALVE_CHARS 8 // max length of valve name displayed 23 | #define STR_IDLE "idle" // idle heating string 24 | #define STR_OK " OK " // string for "no open window" 25 | #define STR_WIFI_ERROR "WiFi connect failed" // error string displayed on ePaper if no wifi connected 26 | #define DEEP_SLEEP_INTERVAL 60 // how many seconds will be esp in deep sleep 27 | 28 | // images start 29 | const unsigned char gImage_heatingIcon[512] = { /* 0X01,0X01,0X40,0X00,0X40,0X00, */ 30 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 31 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 32 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 33 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 34 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 35 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 36 | 0X00,0X00,0X00,0X00,0X07,0XFF,0X80,0X00,0X00,0X00,0X00,0X00,0X3F,0XFF,0XE0,0X00, 37 | 0X00,0X00,0X00,0X01,0XFF,0XFF,0XF8,0X00,0X00,0X00,0X00,0X0F,0XFF,0XFF,0XFE,0X00, 38 | 0X00,0X00,0X00,0X3F,0XFF,0XFF,0XFF,0X00,0X00,0X00,0X00,0X1F,0XFF,0XFF,0XFF,0X80, 39 | 0X00,0X00,0X00,0X0F,0XFF,0XFF,0XFF,0XC0,0X00,0X00,0X00,0X03,0XFF,0XFF,0XFF,0XE0, 40 | 0X00,0X00,0X00,0X00,0X3F,0XFF,0XFF,0XE0,0X00,0X00,0X00,0X00,0X0F,0XFF,0XFF,0XF0, 41 | 0X00,0X00,0X00,0X00,0X0F,0XFF,0XFF,0XF8,0X00,0X00,0X01,0XE0,0X07,0XFF,0XFF,0XF8, 42 | 0X00,0X00,0X07,0XFE,0X0F,0XFF,0XFF,0XFC,0X00,0X00,0X0F,0XFF,0X9F,0XFF,0XDF,0XFC, 43 | 0X00,0X00,0X1F,0XFF,0XFF,0XFC,0X03,0XFC,0X00,0X00,0X3F,0XFF,0XFF,0XF8,0X01,0XFC, 44 | 0X00,0X00,0XFF,0XFF,0XFF,0XFE,0X00,0XFE,0X00,0X01,0XFF,0XFF,0XFF,0XFF,0X00,0X7E, 45 | 0X00,0X07,0XFF,0XFF,0XFF,0XC0,0X00,0X7E,0X00,0X1F,0XFF,0XFF,0XFF,0X80,0X00,0X7E, 46 | 0X00,0XFF,0XFF,0XFF,0XFE,0X00,0X00,0X7E,0X3F,0XFF,0XFF,0XFF,0XF0,0X00,0X00,0X7E, 47 | 0X1F,0XFF,0XFF,0XFF,0XF8,0X00,0X00,0X7E,0X0F,0XFF,0XFF,0XFF,0XFC,0X00,0X00,0X7E, 48 | 0X07,0XFF,0XFF,0XFF,0XFF,0XF8,0X00,0XFE,0X03,0XFF,0XFF,0XFF,0XFF,0XE0,0X00,0XFC, 49 | 0X01,0XFF,0XFF,0XFF,0XFF,0XF8,0X01,0XFC,0X00,0XFF,0XFF,0XFF,0XFF,0XFE,0X07,0XFC, 50 | 0X00,0X3F,0XFF,0XFF,0XFF,0XFF,0XFF,0XF8,0X00,0X0F,0XFF,0XFF,0XFF,0XFF,0XFF,0XF8, 51 | 0X00,0X00,0X00,0X0F,0XFF,0XFF,0XFF,0XF8,0X00,0X00,0X00,0X07,0XFF,0XFF,0XFF,0XF0, 52 | 0X00,0X00,0X00,0XFF,0XFF,0XFF,0XFF,0XE0,0X00,0X00,0X03,0XFF,0XFF,0XFF,0XFF,0XC0, 53 | 0X00,0X00,0X07,0XFF,0XFF,0XFF,0XFF,0X80,0X00,0X00,0X00,0X3F,0XFF,0XFF,0XFF,0X00, 54 | 0X00,0X00,0X00,0X0F,0XFF,0XFF,0XFE,0X00,0X00,0X00,0X00,0X03,0XFF,0XFF,0XFC,0X00, 55 | 0X00,0X00,0X00,0X00,0X7F,0XFF,0XF0,0X00,0X00,0X00,0X00,0X00,0X0F,0XFF,0XC0,0X00, 56 | 0X00,0X00,0X00,0X00,0X00,0X38,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 57 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 58 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 59 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 60 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 61 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 62 | }; 63 | 64 | const unsigned char gImage_openWindow[512] = { /* 0X01,0X01,0X40,0X00,0X40,0X00, */ 65 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 66 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X0F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF0, 67 | 0X1F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF8,0X1F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF8, 68 | 0X0F,0X00,0X30,0X01,0X80,0X0C,0X00,0XF0,0X0F,0X00,0X30,0X01,0X80,0X0C,0X00,0XF0, 69 | 0X07,0X00,0X18,0X01,0X80,0X1C,0X00,0XE0,0X07,0X80,0X18,0X01,0X80,0X18,0X01,0XE0, 70 | 0X03,0X80,0X19,0X8D,0XB0,0XD8,0X01,0XC0,0X03,0XC0,0X19,0X8D,0XB0,0X98,0X03,0XC0, 71 | 0X01,0XC0,0X1B,0X0D,0XA1,0XB0,0X03,0X80,0X01,0XE0,0X0F,0X19,0XE1,0XB0,0X07,0X80, 72 | 0X00,0XE0,0X0C,0X01,0X80,0X30,0X07,0X00,0X00,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0X00, 73 | 0X00,0X7F,0XFF,0XFF,0XFF,0XFF,0XFE,0X00,0X00,0X7F,0XFF,0XFF,0XFF,0XFF,0XFE,0X00, 74 | 0X00,0X78,0X00,0X00,0X00,0X00,0X1E,0X00,0X00,0X70,0X0C,0X21,0X86,0X00,0X0E,0X00, 75 | 0X00,0X70,0X0C,0X61,0X86,0X00,0X0E,0X00,0X00,0X70,0X0C,0X61,0X86,0X00,0X0E,0X00, 76 | 0X00,0X70,0X0C,0X61,0X86,0X00,0X0E,0X00,0X00,0X70,0X0C,0X61,0X86,0X00,0X0E,0X00, 77 | 0X00,0X70,0X0C,0X61,0X86,0X00,0X0E,0X00,0X00,0X70,0X0C,0X31,0X86,0X00,0X0E,0X00, 78 | 0X00,0X70,0X0C,0X30,0XC6,0X00,0X0E,0X00,0X00,0X70,0X06,0X30,0XC3,0X00,0X0E,0X00, 79 | 0X00,0X70,0X06,0X38,0XC3,0X00,0X0E,0X00,0X00,0X70,0X07,0X18,0X63,0X80,0X0E,0X00, 80 | 0X00,0X70,0X03,0X1C,0X61,0X80,0X0E,0X00,0X00,0X70,0X03,0X8C,0X31,0XC0,0X0E,0X00, 81 | 0X00,0X70,0X01,0X8E,0X30,0XC0,0X0E,0X00,0X00,0X70,0X01,0XC6,0X18,0XE0,0X0E,0X00, 82 | 0X00,0X70,0X00,0XC7,0X18,0X60,0X0E,0X00,0X00,0X70,0X00,0XE3,0X0C,0X60,0X0E,0X00, 83 | 0X00,0X70,0X00,0X63,0X0C,0X30,0X0E,0X00,0X00,0X70,0X00,0X61,0X8C,0X30,0X0E,0X00, 84 | 0X00,0X70,0X00,0X71,0X86,0X30,0X0E,0X00,0X00,0X70,0X00,0X31,0X86,0X18,0X0E,0X00, 85 | 0X00,0X70,0X00,0X31,0X86,0X18,0X0E,0X00,0X00,0X70,0X00,0X31,0X86,0X18,0X0E,0X00, 86 | 0X00,0X70,0X00,0X31,0X86,0X18,0X0E,0X00,0X00,0X70,0X00,0X31,0X86,0X18,0X0E,0X00, 87 | 0X00,0X70,0X00,0X31,0X86,0X18,0X0E,0X00,0X00,0X78,0X00,0X31,0X86,0X38,0X1E,0X00, 88 | 0X00,0X7F,0XFF,0XFF,0XFF,0XF7,0XFE,0X00,0X00,0X7F,0XFF,0XFF,0XFF,0XFF,0XFE,0X00, 89 | 0X00,0XFF,0XFF,0XEF,0XFD,0XF7,0XFF,0X00,0X00,0XE0,0X0C,0XE3,0X8C,0X60,0X07,0X00, 90 | 0X01,0XE0,0X1C,0XC7,0X98,0X70,0X07,0X80,0X01,0XC0,0X18,0XC7,0X98,0X70,0X03,0X80, 91 | 0X03,0XC0,0X19,0X87,0XB8,0XF8,0X03,0XC0,0X03,0X80,0X19,0X8D,0XB0,0XD8,0X01,0XC0, 92 | 0X07,0X80,0X38,0X01,0X80,0X18,0X01,0XE0,0X07,0X00,0X30,0X01,0X80,0X18,0X00,0XE0, 93 | 0X0F,0X00,0X30,0X01,0X80,0X18,0X00,0XF0,0X0F,0X00,0X70,0X01,0X80,0X1C,0X00,0XF0, 94 | 0X1F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF8,0X1F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF8, 95 | 0X0F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF0,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 96 | 0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00, 97 | }; 98 | // images end 99 | 100 | const char* ssid = SSID_NAME; 101 | const char* password = SSID_PWD; 102 | 103 | // initialize display 104 | // WIDTH 176, HEIGHT 264 105 | Epd epd; 106 | unsigned char image[2048]; 107 | Paint paint(image, 24, 264); 108 | 109 | // thermeq3 variables 110 | String t_valves = ""; 111 | String t_valve_switch = ""; 112 | String t_preference = ""; 113 | String t_mode = ""; 114 | String t_profile = ""; 115 | String t_status = ""; 116 | String i_status = ""; 117 | 118 | // max MAX_VAL rooms per EQ3 system 119 | struct room_t { 120 | String nam; 121 | byte pos; 122 | float stmp; 123 | float ctmp; 124 | } t_rooms[MAX_VAL]; 125 | 126 | // open windows 127 | String owl[MAX_VAL]; 128 | byte owl_len = 0; 129 | 130 | void clearTopCanvas() { 131 | paint.SetWidth(88); 132 | paint.SetHeight(88); 133 | paint.Clear(UNCOLORED); 134 | paint.DrawRectangle(1, 1, 87, 87, COLORED); 135 | } 136 | 137 | void clearBottomCanvas() { 138 | paint.SetWidth(88); 139 | paint.SetHeight(132); 140 | paint.Clear(UNCOLORED); 141 | paint.DrawRectangle(1, 1, 131, 87, COLORED); 142 | } 143 | 144 | 145 | char* toChar(String tmpStr) { 146 | boolean where = true; 147 | while (tmpStr.length() < 6) { 148 | if (where) { 149 | tmpStr = " " + tmpStr; 150 | } else { 151 | tmpStr = tmpStr + " "; 152 | } 153 | where = not where; 154 | } 155 | static char tmpChar[7]; 156 | tmpStr.toCharArray(tmpChar, 7); 157 | return tmpChar; 158 | } 159 | 160 | void displayThermeqSetup() { 161 | char charBuf[16]; 162 | clearTopCanvas(); 163 | paint.DrawStringAt(10, 8, toChar(String(t_valve_switch + "%/#" + t_valves)), &Font16, COLORED); 164 | paint.DrawStringAt(10, 26, toChar(t_preference), &Font16, COLORED); 165 | paint.DrawStringAt(10, 44, toChar(t_mode), &Font16, COLORED); 166 | paint.DrawStringAt(10, 60, toChar(t_profile), &Font16, COLORED); 167 | epd.TransmitPartialBlack(paint.GetImage(), 0, 88, paint.GetWidth(), paint.GetHeight()); 168 | } 169 | 170 | char* getRoomString(byte idx){ 171 | String tmpStr = t_rooms[idx].nam; 172 | char fbuff[5]; 173 | byte tmpLen = tmpStr.length(); 174 | static char tmpChar[24]; 175 | 176 | if (tmpLen > MAX_VALVE_CHARS) { 177 | tmpStr = tmpStr.substring(0, MAX_VALVE_CHARS); 178 | } else { 179 | while (tmpStr.length() < MAX_VALVE_CHARS) { 180 | tmpStr += " "; 181 | } 182 | } 183 | 184 | sprintf(fbuff, " %02d%% ", t_rooms[idx].pos); 185 | tmpStr += String(fbuff); 186 | dtostrf(t_rooms[idx].stmp, 2, 1, fbuff); 187 | tmpStr += String(fbuff); 188 | tmpLen = tmpStr.length(); 189 | tmpStr.toCharArray(tmpChar, tmpLen + 1); 190 | 191 | #ifdef DEBUG_PRG 192 | Serial.println(tmpStr); 193 | #endif 194 | 195 | return tmpChar; 196 | } 197 | 198 | void showValves() { 199 | #ifdef DEBUG_PRG 200 | Serial.println("showValves START"); 201 | #endif 202 | clearBottomCanvas(); 203 | // max 7 valves per screen 204 | // rotator to be implemented 205 | for (byte i=0; i<7; i++) { 206 | if (t_rooms[i].nam != "") paint.DrawStringAt(4, 4 + (i * 12), getRoomString(i), &Font12, COLORED); 207 | } 208 | epd.TransmitPartialBlack(paint.GetImage(), 89, 132, paint.GetWidth(), paint.GetHeight()); 209 | #ifdef DEBUG_PRG 210 | Serial.println("showValves STOP"); 211 | #endif 212 | 213 | } 214 | 215 | char* getWinString(byte idx) { 216 | static char tmpChar[24]; 217 | String tmpStr = owl[idx]; 218 | byte tmpLen = tmpStr.length() + 1; 219 | tmpStr.toCharArray(tmpChar, tmpLen); 220 | #ifdef DEBUG_PRG 221 | Serial.println(tmpChar); 222 | #endif 223 | return tmpChar; 224 | } 225 | 226 | void showWindows() { 227 | #ifdef DEBUG_PRG 228 | Serial.println("showWindows START"); 229 | #endif 230 | clearBottomCanvas(); 231 | for (byte i=0; i<4; i++) { 232 | if (owl[i] != "") { 233 | paint.DrawStringAt(4, 4 + (i * 20), getWinString(i), &Font20, COLORED); 234 | } 235 | } 236 | epd.TransmitPartialRed(paint.GetImage(), 89, 0, paint.GetWidth(), paint.GetHeight()); 237 | #ifdef DEBUG_PRG 238 | Serial.println("showWindows STOP"); 239 | #endif 240 | } 241 | 242 | void getData() { 243 | HTTPClient http; 244 | String payload; 245 | DynamicJsonDocument tjson; 246 | DynamicJsonDocument sys; 247 | DynamicJsonDocument tmp; 248 | JsonArray arr; 249 | JsonObject root; 250 | 251 | #ifdef DEBUG_PRG 252 | Serial.println("getData START"); 253 | #endif 254 | http.begin("http://10.60.0.11:8180/bridge.json"); 255 | 256 | int httpCode = http.GET(); 257 | 258 | if (httpCode > 0) { 259 | #ifdef DEBUG_PRG 260 | Serial.printf("[HTTP] GET... return code: %d\n", httpCode); 261 | #endif 262 | // file found at server 263 | if (httpCode == HTTP_CODE_OK) { 264 | payload = http.getString(); 265 | #ifdef DEBUG_PRG 266 | // Serial.println(payload); 267 | #endif 268 | } 269 | } else { 270 | #ifdef DEBUG_PRG 271 | Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); 272 | #endif 273 | } 274 | http.end(); 275 | 276 | DeserializationError error = deserializeJson(tjson, payload); 277 | if (error) { 278 | #ifdef DEBUG_PRG 279 | Serial.print("deserializeJson() failed: "); 280 | Serial.println(error.c_str()); 281 | #endif 282 | return; 283 | } 284 | 285 | root = tjson.as(); 286 | t_valves = root["valves"].as(); 287 | t_valve_switch = root["valve_switch"].as(); 288 | t_preference = root["preference"].as(); 289 | t_mode = root["mode"].as(); 290 | t_profile = root["profile"].as(); 291 | t_status = root["status_key"].as(); 292 | 293 | // unpack system status 294 | error = deserializeJson(sys, root["system_status"].as()); 295 | if (error) { 296 | #ifdef DEBUG_PRG 297 | Serial.print("deserializeJson() failed: "); 298 | Serial.println(error.c_str()); 299 | #endif 300 | return; 301 | } 302 | JsonObject v_root = sys.as(); 303 | 304 | // unpack rooms 305 | byte i = 0; 306 | JsonObject tmpRoot; 307 | for (JsonObject::iterator it=v_root.begin(); it!=v_root.end(); ++it) { 308 | t_rooms[i].nam = it->key().c_str(); 309 | 310 | error = deserializeJson(tmp, v_root[it->key().c_str()].as()); 311 | tmpRoot = tmp.as(); 312 | // unpack room valves 313 | byte j = 0; 314 | word valve_pos = 0; 315 | float temp = 0.0; 316 | float curr_temp = 0.0; 317 | for (JsonObject::iterator it=tmpRoot.begin(); it!=tmpRoot.end(); ++it) { 318 | arr = it->value().as(); 319 | 320 | valve_pos += arr.get(1); 321 | temp += arr.get(2); 322 | curr_temp += arr.get(3); 323 | j++; 324 | } 325 | t_rooms[i].pos = byte(valve_pos / j); 326 | t_rooms[i].stmp = float(temp / j); 327 | t_rooms[i].ctmp = float(curr_temp / j); 328 | i++; 329 | } 330 | 331 | // unpack open window list 332 | error = deserializeJson(tmp, root["open_window_list"].as()); 333 | if (error) { 334 | #ifdef DEBUG_PRG 335 | Serial.print("deserializeJson() failed: "); 336 | Serial.println(error.c_str()); 337 | #endif 338 | return; 339 | } 340 | tmpRoot = tmp.as(); 341 | i = 0; 342 | for (JsonObject::iterator it=tmpRoot.begin(); it!=tmpRoot.end(); ++it) { 343 | arr = it->value().as(); 344 | owl[i] = arr.get(1); 345 | i++; 346 | } 347 | owl_len = i; 348 | #ifdef DEBUG_PRG 349 | Serial.println("getData STOP"); 350 | #endif 351 | } 352 | 353 | void displayHeating() { 354 | byte h = 0; 355 | byte v = 0; 356 | #ifdef DEBUG_PRG 357 | Serial.println("displayHeating START"); 358 | #endif 359 | if (i_status != t_status) { 360 | if (t_status.charAt(0) == 'h') { 361 | h = 1; 362 | } 363 | if (t_status.charAt(1) == 'v' || owl_len > 0) { 364 | v = 1; 365 | } 366 | #ifdef DEBUG_PRG 367 | Serial.print("h="); 368 | Serial.println(h); 369 | Serial.print("v="); 370 | Serial.println(v); 371 | #endif 372 | clearTopCanvas(); 373 | if (h == 1) { 374 | if (v == 0) { 375 | epd.TransmitPartialRed(gImage_heatingIcon, 11, 189, 64, 64); 376 | } else { 377 | epd.TransmitPartialBlack(gImage_heatingIcon, 11, 189, 64, 64); 378 | } 379 | } else { 380 | paint.DrawStringAt(8, 32, STR_IDLE, &Font24, COLORED); 381 | } 382 | epd.TransmitPartialBlack(paint.GetImage(), 0, 176, paint.GetWidth(), paint.GetHeight()); 383 | 384 | // windows 385 | clearTopCanvas(); 386 | if (v == 1) { 387 | epd.TransmitPartialRed(gImage_openWindow, 12, 12, 64, 64); 388 | } else { 389 | paint.DrawStringAt(8, 32, STR_OK, &Font24, COLORED); 390 | } 391 | i_status = t_status; 392 | } 393 | epd.TransmitPartialBlack(paint.GetImage(), 0, 0, paint.GetWidth(), paint.GetHeight()); 394 | #ifdef DEBUG_PRG 395 | Serial.println("displayHeating STOP"); 396 | #endif 397 | } 398 | 399 | boolean connect() { 400 | // WiFi.persistent(false); 401 | WiFi.mode(WIFI_STA); 402 | WiFi.begin(ssid, password); 403 | 404 | #ifdef DEBUG_PRG 405 | Serial.println("thermeq3 eDisplay"); 406 | Serial.println("-----------------"); 407 | Serial.print("Connected to "); 408 | Serial.println(ssid); 409 | #endif 410 | 411 | // Wait for connection 412 | // max 15 sec 413 | byte i = 0; 414 | while (WiFi.status() != WL_CONNECTED) { 415 | delay(1000); 416 | if (i > 15) { 417 | break; 418 | } 419 | #ifdef DEBUG_PRG 420 | Serial.print("."); 421 | #endif 422 | } 423 | 424 | if (i > 15) return false; 425 | else { 426 | #ifdef DEBUG_PRG 427 | Serial.print("\r\nIP address: "); 428 | Serial.println(WiFi.localIP()); 429 | #endif 430 | return true; 431 | } 432 | } 433 | 434 | bool saveConfig() { 435 | File configFile = SPIFFS.open("/status.cfg", "w"); 436 | if (!configFile) { 437 | #ifdef DEBUG_PRG 438 | Serial.println("Failed to open config file for writing"); 439 | #endif 440 | return false; 441 | } 442 | configFile.println(i_status); 443 | configFile.close(); 444 | return true; 445 | } 446 | 447 | bool loadConfig() { 448 | // open file for reading 449 | #ifdef DEBUG_PRG 450 | Serial.println("loadConfig START"); 451 | #endif 452 | File configFile = SPIFFS.open("/status.cfg", "r"); 453 | if (!configFile) { 454 | #ifdef DEBUG_PRG 455 | Serial.println("file open failed"); 456 | #endif 457 | } 458 | String tmpStr = configFile.readStringUntil('\n'); 459 | i_status = tmpStr; 460 | configFile.close(); 461 | #ifdef DEBUG_PRG 462 | Serial.println(tmpStr); 463 | #endif 464 | #ifdef DEBUG_PRG 465 | Serial.println("loadConfig STOP"); 466 | #endif 467 | } 468 | 469 | void setup() { 470 | // initialize serial port 471 | #ifdef DEBUG_PRG 472 | Serial.begin(115200); 473 | while (!Serial); 474 | #endif 475 | 476 | // if ePaper is not present, quit 477 | if (epd.Init() != 0) { 478 | #ifdef DEBUG_PRG 479 | Serial.print("e-Paper init failed"); 480 | #endif 481 | return; 482 | } 483 | 484 | if (!SPIFFS.begin()) { 485 | #ifdef DEBUG_PRG 486 | Serial.println("Failed to mount file system"); 487 | #endif 488 | return; 489 | } 490 | 491 | loadConfig(); 492 | // clear display 493 | epd.ClearFrame(); 494 | // rotate to landscape 495 | paint.SetRotate(ROTATE_270); 496 | 497 | // if connected, get data, display 498 | if (connect()) { 499 | getData(); 500 | 501 | // display data 502 | displayHeating(); 503 | displayThermeqSetup(); 504 | showValves(); 505 | showWindows(); 506 | saveConfig(); 507 | } else { 508 | paint.DrawStringAt(0, 0, STR_WIFI_ERROR, &Font24, COLORED); 509 | epd.TransmitPartialRed(paint.GetImage(), 8, 16, paint.GetWidth(), paint.GetHeight()); 510 | } 511 | 512 | epd.DisplayFrame(); 513 | // deep sleep for epaper 514 | epd.Sleep(); 515 | 516 | ESP.deepSleep(DEEP_SLEEP_INTERVAL * 1000000); 517 | } 518 | 519 | void loop() { 520 | 521 | } 522 | -------------------------------------------------------------------------------- /obsolete/lib/maxeq3.py: -------------------------------------------------------------------------------- 1 | """ 2 | EQ-3/ELV MAX! communication 3 | """ 4 | # imports 5 | import socket 6 | import time 7 | import base64 8 | import datetime 9 | import json 10 | 11 | """ 12 | new devices dictionary 13 | devices = { 14 | address: { 15 | type: string, 16 | serial: string, 17 | name: string, 18 | if type=1 then {pos: int, temp: float, c_temp: float} 19 | if type=4 then {open: boolean, ow_time: datetime} 20 | status: [status, info], 21 | other: [temp_offset] 22 | } 23 | } 24 | """ 25 | 26 | 27 | class EQ3Data: 28 | def __init__(self, ip_address, port): 29 | self.max_ip = ip_address 30 | self.max_port = port 31 | self.timeout = 10 32 | # 33 | # dictionaries and lists 34 | # 35 | self.maxid = {"sn": "000000", "rf": "", "fw": ""} 36 | # valves = {devices-addr: [0/valve_pos, 1/valve_temp, 2/valve_curtemp, 3/valve_name]} 37 | self.valves = {} 38 | # rooms = {id : [0/room_name, 1/room_address, 2/is_win_open, 3/curr_temp, 4/average valve position]} 39 | self.rooms = {} 40 | # devices = {address: [0/type, 1/serial, 2/name, 3/room:id, 4/OW, 5/OW_time, 6/status, 7/info, 8/temp offset]} 41 | self.devices = {} 42 | # device log, used to monitor valve positions, if many same positions than probably error 43 | self.device_log = {} 44 | # opened windows = {key: OW_time(now), isMuted(False), warning/error count(0)} 45 | self.windows = {} 46 | # ignored valves 47 | self.ignored_valves = {} 48 | # to save Exception and e values 49 | self.return_error = [] 50 | # logger messages queue 51 | self.log_messages = [] 52 | # how many minutes after closing window is valve ignored 53 | self.ignore_time = 0 54 | # communication errors count 55 | self.comm_error = 0 56 | # how many times try connect to MAX!Cube 57 | self.max_iteration = 3 58 | # outside init declaration 59 | self.client_socket = None 60 | # set zero temp from HT or not 61 | self.set_zero = True 62 | # which values will be written into csv file 63 | self.csv_values = 1 64 | 65 | def __repr__(self): 66 | return str(self.__class__) + ": " + str(self.__dict__) 67 | 68 | def __str__(self): 69 | return str(self.__class__) + ": " + str(self.__dict__) 70 | 71 | def is_same(self, key, error_interval=2): 72 | """ 73 | Problem prediction routine, if during heating valve didn't change position, something is wrong 74 | :param key: string, key 75 | :param error_interval: integer, plus/minus value 76 | :return: boolean 77 | """ 78 | tmp = self.device_log[key][1] 79 | kv = self.valves[key][1] 80 | if tmp - error_interval <= kv <= tmp + error_interval: 81 | return True 82 | else: 83 | return False 84 | 85 | def get_full_name(self, k): 86 | """ 87 | Return complete names for valve, room 88 | :param k: key 89 | :return: list [room name, device name, valve list] 90 | """ 91 | v = self.valves[k] 92 | dn = self.devices[k][2] 93 | rn = self.rooms[str(self.devices[k][3])][0] 94 | return [rn, dn, v] 95 | 96 | def get_full_name_tilda(self, k): 97 | """ 98 | Return complete names for valve, room 99 | :param k: key 100 | :return: list [room name, device name, valve list] 101 | """ 102 | v = self.valves[k] 103 | dn = self.devices[k][2] 104 | rn = self.rooms[str(self.devices[k][3])][0] + "~" + self.rooms[str(self.devices[k][3])][1] 105 | return [rn, dn, v] 106 | 107 | def get_key_full_name(self, k): 108 | """ 109 | Return complete names for valve, room as dictionary 110 | :param k: key 111 | :return: dictionary {key: [room name, device name, valve list for key]} 112 | """ 113 | return {k: self.get_full_name(k)} 114 | 115 | def device_name(self, k): 116 | """ 117 | Return device name 118 | :param k: key 119 | :return: valve name/string 120 | # devices = {addr: [type, serial, name, room, OW, OW_time, status, info, temp offset]} 121 | """ 122 | dn = str(self.devices[k][2]) 123 | return dn 124 | 125 | def get_room_name(self, k): 126 | """ 127 | Return device room name for device key/address 128 | :param k: 129 | :return: room name/string 130 | """ 131 | rn = self.rooms[str(self.devices[k][3])][0] 132 | return rn 133 | 134 | @staticmethod 135 | def is_valid(valve_info): 136 | """ 137 | :param valve_info: valve status info 138 | :return: boolean, true if valid 139 | """ 140 | return bool((valve_info & 0b00010000) >> 4) 141 | 142 | def is_radio_error(self, key): 143 | """ 144 | True if radio error on device with key 145 | :param key: key 146 | :return: Boolean 147 | """ 148 | v = self.devices[key] 149 | if v[6] & 8 == 8: 150 | return True 151 | else: 152 | return False 153 | 154 | def is_battery_error(self, key): 155 | """ 156 | True if battery error on device with key 157 | :param key: key 158 | :return: boolean 159 | """ 160 | """ True if battery error on device with key """ 161 | v = self.devices[key] 162 | if v[7] & 128 == 128: 163 | return True 164 | else: 165 | return False 166 | 167 | def is_window_open(self, key): 168 | """ 169 | Return true if window is open 170 | :param key: key 171 | :return: boolean 172 | """ 173 | v = self.devices[key] 174 | if v[0] == 4 and v[4] == 2: 175 | return True 176 | else: 177 | return False 178 | 179 | def count_valve(self, key): 180 | """ 181 | Return True if valve is NOT ignored 182 | :param key: key 183 | :return: boolean 184 | """ 185 | if key in self.ignored_valves: 186 | if self.ignored_valves[key] < time.time(): 187 | del self.ignored_valves[key] 188 | else: 189 | return False 190 | return True 191 | 192 | @staticmethod 193 | def _hexify(addr): 194 | """ 195 | Returns hexified address 196 | :param addr: string, device address 197 | :return: string 198 | """ 199 | return "".join("%02x" % ord(c) for c in addr).upper() 200 | 201 | def _inc_error(self): 202 | """ 203 | Increment error variable 204 | :return: nothing 205 | """ 206 | self.comm_error += 1 207 | 208 | def _read_lines(self, recv_buffer=4096, delimiter="\r\n"): 209 | lines_buffer = "" 210 | data = True 211 | while data: 212 | try: 213 | data = self.client_socket.recv(recv_buffer) 214 | lines_buffer += data 215 | while lines_buffer.find(delimiter) != -1: 216 | line, lines_buffer = lines_buffer.split("\n", 1) 217 | yield line 218 | except socket.timeout: 219 | return 220 | return 221 | 222 | def _set_ignored_valves(self, key): 223 | room = self.devices[key][3] 224 | for k, v in self.devices.iteritems(): 225 | # this is heating thermostat and is in room where we want ignore all heating thermostats 226 | # and is not in ignored_valves dictionary = is not ignored now 227 | if v[0] == 1 and v[3] == room and k not in self.ignored_valves: 228 | # don't heat X*60 seconds after closing window 229 | self.ignored_valves.update({k: time.time() + self.ignore_time * 60}) 230 | 231 | def open(self): 232 | """ open communication to MAX! Cube """ 233 | self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 234 | # on some system use code below: 235 | # self.client_socket.setblocking(0) 236 | self.client_socket.settimeout(int(self.timeout / 2)) 237 | 238 | self.max_iteration = 3 239 | _i = 0 240 | _result = False 241 | while _i < self.max_iteration: 242 | try: 243 | self.client_socket.connect((self.max_ip, self.max_port)) 244 | except Exception, e: 245 | self._inc_error() 246 | _i += 1 247 | # wait predefined time 248 | time.sleep(int(self.timeout / self.max_iteration)) 249 | # self.return_error.append([Exception, e, str(traceback.format_exc())]) 250 | self.return_error.append("Timeout. Operation in progress.\n") 251 | else: 252 | _i = self.max_iteration 253 | _result = True 254 | # return result, if False then return_error contains error 255 | return _result 256 | 257 | def close(self): 258 | """ close connection to MAX """ 259 | self.client_socket.close() 260 | 261 | def _process(self): 262 | """ 263 | Update room average valve position 264 | :return: nothing 265 | """ 266 | tmp_room = {} 267 | for k, v in self.valves.iteritems(): 268 | room_id = self.get_room_name(k) 269 | if self.count_valve(k): 270 | if room_id not in tmp_room: 271 | position = 0 272 | count = 1 273 | else: 274 | position = tmp_room[room_id][0] 275 | count = tmp_room[room_id][1] + 1 276 | tmp_room.update({room_id: [position + v[0], count]}) 277 | 278 | # erase rooms average valve position 279 | for k, v in self.rooms.iteritems(): 280 | self.rooms[k][4] = 0 281 | # set rooms average valve position 282 | for k, v in tmp_room.iteritems(): 283 | try: 284 | result = v[0] / v[1] 285 | except ZeroDivisionError: 286 | result = 0 287 | finally: 288 | r_id = self.get_room_id_by_name(k) 289 | if k is not None: 290 | self.rooms[r_id][4] = result 291 | 292 | def get_room_id_by_name(self, r_name): 293 | for k, v in self.rooms.iteritems(): 294 | if v[0] == r_name: 295 | return k 296 | return None 297 | 298 | def read_data(self, refresh=False): 299 | """ 300 | :param refresh: passable argument 301 | :return: boolean, list 302 | """ 303 | result = False 304 | if self.open(): 305 | self.read(refresh) 306 | self._process() 307 | result = True 308 | # close session 309 | self.close() 310 | return result, self.return_error 311 | 312 | def read(self, refresh): 313 | """ 314 | read data from MAX! cube 315 | :param refresh: boolean 316 | :return: nothing 317 | """ 318 | self.client_socket.settimeout(int(self.timeout / 3)) 319 | for line in self._read_lines(): 320 | data = line 321 | sd = data[2:].split(",") 322 | if data[0] == 'H': 323 | self.cmd_h(sd) 324 | elif data[0] == 'M': 325 | self.cmd_m(sd, refresh) 326 | elif data[0] == 'C': 327 | self.cmd_c(sd) 328 | elif data[0] == 'L': 329 | self.cmd_l(sd) 330 | 331 | def delete(self, dev_id): 332 | """ 333 | Delete device from cube 334 | :param dev_id: device id 335 | :return: boolean 336 | """ 337 | result = None 338 | if self.open(): 339 | self._read_lines() 340 | dev_id_plain = bytearray.fromhex(dev_id).decode() 341 | message = "t:01,1," + base64.b64encode(dev_id_plain) + "\r\n" 342 | try: 343 | self.client_socket.sendall(message) 344 | except socket.error: 345 | result = False 346 | else: 347 | result = True 348 | finally: 349 | self.close() 350 | return result 351 | 352 | def cmd_h(self, line): 353 | """ 354 | Process H response 355 | :param line: string 356 | :return: nothing 357 | """ 358 | self.maxid["sn"] = line[0] 359 | self.maxid["rf"] = line[1] 360 | self.maxid["fw"] = line[2] 361 | 362 | def cmd_m(self, line, refresh): 363 | """ 364 | Process H response 365 | :param line: string 366 | :param refresh: boolean 367 | :return: 368 | """ 369 | if len(line) < 2: 370 | self.return_error.append("Error reading M response. Probably cube without setup!") 371 | else: 372 | es = base64.b64decode(line[2]) 373 | room_num = ord(es[2]) 374 | es_pos = 3 375 | this_now = datetime.datetime.now() 376 | for _ in range(0, room_num): 377 | room_id = str(ord(es[es_pos])) 378 | room_len = ord(es[es_pos + 1]) 379 | es_pos += 2 380 | room_name = es[es_pos:es_pos + room_len] 381 | es_pos += room_len 382 | room_adr = es[es_pos:es_pos + 3] 383 | es_pos += 3 384 | if room_id not in self.rooms or refresh: 385 | # id : 0room_name, 1room_address, 2is_win_open, 3curr_temp 386 | self.rooms.update({room_id: [room_name, self._hexify(room_adr), False, 99.99, 0]}) 387 | dev_num = ord(es[es_pos]) 388 | es_pos += 1 389 | for _ in range(0, dev_num): 390 | dev_type = ord(es[es_pos]) 391 | es_pos += 1 392 | dev_adr = self._hexify(es[es_pos:es_pos + 3]) 393 | es_pos += 3 394 | dev_sn = es[es_pos:es_pos + 10] 395 | es_pos += 10 396 | dev_len = ord(es[es_pos]) 397 | es_pos += 1 398 | dev_name = es[es_pos:es_pos + dev_len] 399 | es_pos += dev_len 400 | dev_room = ord(es[es_pos]) 401 | es_pos += 1 402 | if dev_adr not in self.devices or refresh: 403 | # 0type 1serial 2name 3room 4OW,5OW_time, 6status, 7info, 8temp offset 404 | self.devices.update({dev_adr: [dev_type, dev_sn, dev_name, dev_room, 0, this_now, 0, 0, 7]}) 405 | 406 | def cmd_c(self, line): 407 | """ 408 | process C response 409 | :param line: string 410 | :return: nothing 411 | """ 412 | es = base64.b64decode(line[1]) 413 | if ord(es[0x04]) == 1: 414 | dev_adr = self._hexify(es[0x01:0x04]) 415 | self.devices[dev_adr][8] = es[0x16] 416 | 417 | def cmd_l(self, line): 418 | """ 419 | process L response 420 | :param line: string 421 | :return: nothing 422 | """ 423 | es = base64.b64decode(line[0]) 424 | es_pos = 0 425 | while es_pos < len(es): 426 | dev_len = ord(es[es_pos]) 427 | valve_adr = self._hexify(es[es_pos + 1:es_pos + 4]) 428 | if valve_adr in self.devices: 429 | valve_status = ord(es[es_pos + 0x06]) 430 | valve_info = ord(es[es_pos + 0x05]) 431 | valve_temp = 0xFF 432 | valve_curr_temp = 0xFF 433 | # WallMountedThermostat (dev_type 3) 434 | if dev_len == 12: 435 | if self.is_valid(valve_info): 436 | # lsb = ord(es[es_pos + 0x08]) & 0b01111111 437 | tmp = ord(es[es_pos + 0x08]) & 0b10000000 438 | msb = (tmp >> 7) * 256 439 | # get set temp, on wall thermostat no reason for set temp 440 | # valve_temp = float(lsb) / 2 441 | # get measured temp 442 | lsb = ord(es[es_pos + 0x0C]) 443 | valve_curr_temp = float(msb + lsb) / 10 444 | # extract room name from this WallMountedThermostat 445 | wall_room_id = str(self.devices[valve_adr][3]) 446 | # and update its value to current temperature as read from wall mounted thermostat 447 | self.rooms[wall_room_id][3] = valve_curr_temp 448 | # HeatingThermostat (dev_type 1 or 2) 449 | elif dev_len == 11: 450 | valve_pos = ord(es[es_pos + 0x07]) 451 | if self.is_valid(valve_info): 452 | lsb = ord(es[es_pos + 0x08]) & 0b01111111 453 | tmp = ord(es[es_pos + 0x08]) & 0b10000000 454 | msb = (tmp >> 7) * 256 455 | # get set temp 456 | valve_temp = float(lsb) / 2 457 | # extract room name from this HeatingThermostat 458 | valve_room_id = str(self.devices[valve_adr][3]) 459 | # if room temp not set i.e. still returning default 99.99 460 | if self.rooms[valve_room_id][3] == 99.99: 461 | # get room temp from this valve 462 | lsb = ord(es[es_pos + 0x09]) 463 | valve_curr_temp = float(msb + lsb) / 10 464 | # and update room temp too 465 | self.rooms[valve_room_id][3] = valve_curr_temp 466 | else: 467 | # read room temp which was earlier set by wall thermostat 468 | valve_curr_temp = self.rooms[valve_room_id][3] 469 | # check if current temperature is 0.0 and if set_zero is disabled, then get "obsolete actual" temp, 470 | # just to have no 0.0 in log or CSV 471 | if valve_curr_temp == 0.0 and not self.set_zero: 472 | valve_curr_temp = self.valves[valve_adr][2] 473 | self.valves.update({valve_adr: [valve_pos, valve_temp, valve_curr_temp]}) 474 | # WindowContact 475 | elif dev_len == 6: 476 | tmp_open = ord(es[es_pos + 0x06]) & 2 477 | # if state changed 478 | if tmp_open != self.devices[valve_adr][4]: 479 | # get room id 480 | r_id = str(self.devices[valve_adr][3]) 481 | # if window is now closed 482 | if tmp_open == 0: 483 | self.rooms[r_id][2] = False 484 | if valve_adr in self.windows: 485 | del self.windows[valve_adr] 486 | # check for window closed ignore interval, if non zero, set valves to ignore 487 | if self.ignore_time > 0: 488 | self._set_ignored_valves(valve_adr) 489 | else: 490 | # or is opened now 491 | self.rooms[r_id][2] = True 492 | self.devices[valve_adr][4] = tmp_open 493 | self.devices[valve_adr][5] = datetime.datetime.now() 494 | # save status and info 495 | self.devices[valve_adr][6] = valve_status 496 | self.devices[valve_adr][7] = valve_info 497 | 498 | es_pos += (dev_len + 1) 499 | else: 500 | # some error logging 501 | # if this is annoying please comment lines below 502 | if len(self.return_error) > 0: 503 | self.return_error.append(str(valve_adr)) 504 | else: 505 | self.return_error.append("Address error. Please use del_dev command. Key(s): ") 506 | self.return_error.append(str(valve_adr)) 507 | # and uncomment line below 508 | # pass 509 | 510 | def csv(self): 511 | """ format csv string, the second part, just valves """ 512 | tmp = "" 513 | for k, v in self.valves.iteritems(): 514 | # thanks to Wojciech from Polska this is configurable 515 | if self.csv_values == 3: 516 | # this uses both temps 517 | tmp += str(v[0]) + "," + str(v[1]) + "," + str(v[2]) + "," 518 | elif self.csv_values == 2: 519 | # this is current temp 520 | tmp += str(v[0]) + "," + str(v[2]) + "," 521 | else: 522 | # this line uses valve set temp 523 | tmp += str(v[0]) + "," + str(v[1]) + "," 524 | return tmp 525 | 526 | def plain(self): 527 | rooms = {} 528 | # id : 0room_name, 1room_address, 2is_win_open, 3curr_temp 529 | for k, v in self.rooms.iteritems(): 530 | rooms.update({str(v[0] + "~" + str(v[1])): ["", v[2], v[4]]}) 531 | 532 | for k, v in self.valves.iteritems(): 533 | # update rooms string 534 | room_id = str(self.get_full_name_tilda(k)[0]) 535 | room_str = rooms[room_id][0] 536 | room_str += "\n\t[" + str(k) + "] " + '{:<20}'.format(str(self.devices[k][2])) + "@" + \ 537 | '{:>3}'.format(str(v[0])) + "% @ " + \ 538 | '{:>4}'.format(str(v[1])) + "'C # " + '{:>4}'.format(str(v[2])) + "'C " 539 | cv = self.count_valve(k) 540 | if cv: 541 | room_str += "(+)" 542 | else: 543 | room_str += "(-) till " + time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(self.ignored_valves[k])) 544 | 545 | rooms[room_id][0] = room_str 546 | 547 | log_str = "Actual positions:" 548 | for k, v in rooms.iteritems(): 549 | log_str += "\nRoom: " + str(k).split('~')[0] 550 | if v[1]: 551 | log_str += ", open window" 552 | if v[0].count("\n\t") > 1: 553 | log_str += ", average: " + str(v[2]) + "%" 554 | log_str += str(v[0]) 555 | return log_str 556 | 557 | def json_status(self): 558 | """ 559 | Return json in format 560 | {room_name: {valve_addr: {valve_name, valve_position, set_temp, current_temp, count_valve, ignore_until}}} 561 | :return: json 562 | """ 563 | # devices = {addr: [type, serial, name, room, OW, OW_time, status, info, temp offset]} 564 | # rooms = {id: [room_name, room_address, is_win_open, curr_temp]} 565 | # valves = {valve_adr: [valve_pos, valve_temp, valve_curr_temp]} 566 | rooms = {} 567 | current = {} 568 | for k, v in self.rooms.iteritems(): 569 | rooms.update({str(v[0]): ["", v[2]]}) 570 | current.update({str(v[0]): {}}) 571 | 572 | for k, v in self.valves.iteritems(): 573 | # update rooms string 574 | room_id = self.get_room_name(k) 575 | cv = self.count_valve(k) 576 | if not cv: 577 | till = time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(self.ignored_valves[k])) 578 | else: 579 | till = 0 580 | current[room_id].update({str(k): [str(self.devices[k][2]), str(v[0]), 581 | str(v[1]), str(v[2]), str(1 if cv else 0), 582 | str(till), str(self.devices[k][6])]}) 583 | 584 | return json.dumps(current) 585 | 586 | def json_valve_status(self): 587 | current = {} 588 | for k, v in self.rooms.iteritems(): 589 | current.update({str(v[0]): {}}) 590 | 591 | for k, v in self.valves.iteritems(): 592 | room_id = str(self.get_full_name(k)[0]) 593 | cv = self.count_valve(k) 594 | current[room_id].update( 595 | {str(k): [str(self.devices[k][2]), str(v[0]), str(v[1]), str(v[2]), str(1 if cv else 0)]}) 596 | 597 | return json.dumps(current) 598 | 599 | def nice(self): 600 | tmp_str = self.plain() 601 | tmp_str.replace("\r\n", "
") 602 | tmp_str.replace("\t", " ") 603 | return "\r\n\r\nStatus\r\n\r\n

" + tmp_str + \
604 |                "

\r\n\r\n" 605 | 606 | def headers(self, rn_vn=False): 607 | """ 608 | Returns headers for CSV file in desired format 609 | :param rn_vn: boolean, if true then room name - valve name is generated 610 | :return: headers/string 611 | """ 612 | tmp = "" 613 | for k, v in self.valves.iteritems(): 614 | if rn_vn: 615 | room_id = str(self.get_full_name(k)[0]) 616 | name = self.rooms[room_id][0] + "-" + self.devices[k][2] 617 | else: 618 | name = self.devices[k][2] 619 | # choose headers according to what is written into csv file 620 | # first position is always valve position in % 621 | tmp += name + " (position)," 622 | if self.csv_values == 3: 623 | # both temperatures 624 | tmp += name + " (set temp)," + name + " (actual temp)," 625 | elif self.csv_values == 2: 626 | # actual temp 627 | tmp += name + " (actual temp)," 628 | else: 629 | # set temp 630 | tmp += name + " (set temp)," 631 | 632 | return tmp[:-1] 633 | 634 | def rooms_status(self): 635 | """ returns json in format address: [data] """ 636 | # rooms = {id : [room_name, room_address, is_win_open, curr_temp]} 637 | tmp = {} 638 | for k, v in self.rooms.iteritems(): 639 | tmp.update({v[1]: [v[0], v[2], v[3]]}) 640 | return json.dumps(tmp) 641 | 642 | def valve_status(self): 643 | """ returns json in format address: [data] """ 644 | # valves = {valve_adr: [valve_pos, valve_temp, valve_curr_temp, valve_name]} 645 | tmp = {} 646 | for k, v in self.valves.iteritems(): 647 | valve_name = self.device_name(k) 648 | tmp.update({k: [v[0], v[1], v[2], valve_name]}) 649 | return json.dumps(tmp) 650 | --------------------------------------------------------------------------------