├── LICENSE ├── README.md └── eq3_control.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 mpex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EQ3-Thermostat 2 | 3 | 4 | 5 | Library to control EQ3 BTLE Thermostats. 6 | 7 | It makes use of *gatttool* which is part of the *bluez* package. 8 | 9 | With the lib can do: 10 | 11 | * Read current temperature and lockstate from thermostat (if changed manually) 12 | * Activate Boostmode: 300sec fully open valve 13 | * Deactive Boostmode: Interrupt Boostmode earlier than 300sec 14 | * Lock Thermostat: Disable manual mode 15 | * Unlock Thermostat: Enable manual mode 16 | * Switch between automatic, manual and eco mode (automatic schedule on thermostat side) 17 | * Set Temperature Offset: Set an offset to measured temperature 18 | * Set Day/Night Mode: Change between two preset values 19 | * Change Window Open settings: Change temperature and duration 20 | * Set Temperature: Self explanatory (given in celcius) 21 | * Set Time: Set date/time on the thermostat 22 | 23 | 24 | What the lib cannot do: 25 | 26 | * Check if device is really present. Right now no error handling is done, 27 | as it is hard to determine wether the command was successful or not. 28 | * Manipulation of the inbuilt programs. Not necessary for my needs 29 | * The vacation function is not implemented 30 | 31 | -------------------------------------------------------------------------------- /eq3_control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*-coding: utf-8 -*- 3 | 4 | import datetime 5 | import subprocess 6 | import time 7 | 8 | 9 | class EQ3Thermostat(object): 10 | 11 | def __init__(self, address): 12 | self.address = address 13 | self.locked = False 14 | self.temperature = 0 15 | self.update() 16 | 17 | def update(self): 18 | """Reads the current temperature from the thermostat. We need to kill 19 | the gatttool process as the --listen option puts it into an infinite 20 | loop.""" 21 | p = subprocess.Popen(["timeout", "-s", "INT", "2", "gatttool", "-b", 22 | self.address, "--char-write-req", "-a", "0x0411", 23 | "-n", "03", "--listen"], 24 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 25 | out, err = p.communicate() 26 | value_string = out.decode("utf-8") 27 | 28 | if "Notification handle" in value_string: 29 | value_string_splt = value_string.split() 30 | temperature = value_string_splt[-1] 31 | locked = value_string_splt[-4] 32 | try: 33 | subprocess.Popen.kill(p) 34 | except ProcessLookupError: 35 | pass 36 | 37 | if locked == "20": 38 | self.locked = True 39 | elif locked == "00": 40 | self.locked = False 41 | else: 42 | print("Could not read lockstate of {}".format(self.address)) 43 | 44 | try: 45 | self.temperature = int(temperature, 16) / 2 46 | except Exception as e: 47 | print("Getting temperature of {} failed {}".format(self.address, e)) 48 | 49 | def activate_boostmode(self): 50 | """Boostmode fully opens the thermostat for 300sec.""" 51 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 52 | "-a", "0x0411", "-n", "4501"], 53 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 54 | 55 | def deactivate_boostmode(self): 56 | """Use only to stop boostmode before 300sec.""" 57 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 58 | "-a", "0x0411", "-n", "4500"], 59 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 60 | 61 | def set_automatic_mode(self): 62 | """Put thermostat in automatic mode.""" 63 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 64 | "-a", "0x0411", "-n", "4000"], 65 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 66 | 67 | def set_manual_mode(self): 68 | """Put thermostat in manual mode.""" 69 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 70 | "-a", "0x0411", "-n", "4040"], 71 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 72 | 73 | def set_eco_mode(self): 74 | """Put thermostat in eco mode.""" 75 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 76 | "-a", "0x0411", "-n", "4080"], 77 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 78 | 79 | def lock_thermostat(self): 80 | """Locks the thermostat for manual use.""" 81 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 82 | "-a", "0x0411", "-n", "8001"], 83 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 84 | 85 | def unlock_thermostat(self): 86 | """Unlocks the thermostat for manual use.""" 87 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 88 | "-a", "0x0411", "-n", "8000"], 89 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 90 | 91 | def set_temperature(self, temperature): 92 | """Transform the temperature in celcius to make it readable to the thermostat.""" 93 | temperature = hex(int(2 * float(temperature)))[2:] 94 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 95 | "-a", "0x0411", "-n", "41{}".format(temperature)], 96 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 97 | # Block for 3 secs to let the thermostat adjust the temperature 98 | time.sleep(3) 99 | 100 | def set_temperature_offset(self, offset): 101 | """Untested.""" 102 | temperature = hex(int(2 * float(offset) + 7))[2:] 103 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 104 | "-a", "0x0411", "-n", "13{}".format(temperature)], 105 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 106 | 107 | def set_day(self): 108 | """Puts thermostat into day mode (sun icon).""" 109 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 110 | "-a", "0x0411", "-n", "43"], 111 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 112 | 113 | def set_night(self): 114 | """Puts thermostat into night mode (moon icon).""" 115 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 116 | "-a", "0x0411", "-n", "44"], 117 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 118 | 119 | def set_day_night(self, night, day): 120 | """Sets comfort temperature for day and night.""" 121 | day = hex(int(2 * float(day)))[2:] 122 | night = hex(int(2 * float(night)))[2:] 123 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 124 | "-a", "0x0411", "-n", "11{}{}".format(day, night)], 125 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 126 | 127 | def set_windows_open(self, temperature, duration_min): 128 | """Untested.""" 129 | temperature = hex(int(2 * float(temperature)))[2:] 130 | duration_min = hex(int(duration_min / 5.0))[2:] 131 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 132 | "-a", "0x0411", "-n", "11{}{}".format(temperature, duration_min)], 133 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 134 | 135 | def set_time(self, datetimeobj): 136 | """Takes a datetimeobj (like datetime.datetime.now()) and sets the time 137 | in the thermostat.""" 138 | command_prefix = "03" 139 | year = "{:02X}".format(datetimeobj.year % 100) 140 | month = "{:02X}".format(datetimeobj.month) 141 | day = "{:02X}".format(datetimeobj.day) 142 | hour = "{:02X}".format(datetimeobj.hour) 143 | minute = "{:02X}".format(datetimeobj.minute) 144 | second = "{:02X}".format(datetimeobj.second) 145 | control_string = "{}{}{}{}{}{}{}".format( 146 | command_prefix, year, month, day, hour, minute, second) 147 | p = subprocess.Popen(["gatttool", "-b", self.address, "--char-write-req", 148 | "-a", "0x0411", "-n", control_string], 149 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 150 | # Block for 3 secs to let the thermostat adjust the settings 151 | time.sleep(3) 152 | 153 | if __name__ == '__main__': 154 | h = EQ3Thermostat("00:AA:BB:CC:DD:EE") 155 | # Take some time 156 | time.sleep(5) 157 | # Deactivate autonomous behavior on the thermostat 158 | h.set_manual_mode() 159 | # Set current date 160 | h.set_time(datetime.datetime.now()) 161 | # Set the current temperature 162 | h.set_temperature(20) 163 | --------------------------------------------------------------------------------