├── .gitignore ├── LICENSE ├── README.md ├── img └── rpi_smartplug.jpg ├── old_scripts └── smartplugctl ├── pySmartPlugSmpB16.py ├── requirements.txt ├── scripts ├── smartplugctl └── smartplugscan └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # PyCharm files 65 | .idea/ 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 l.lefebvre 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 | # smartplugctl 2 | 3 | Little utility for control Awox BLE smartPlug. Test with Bluez 5 and Raspberry 4 | Pi. Also test on amd64 debian jessie. 5 | 6 | ![](img/rpi_smartplug.jpg) 7 | 8 | ## Read first 9 | 10 | Two scripts is provide here to deal with the plug(s): smartplugscan for find 11 | the plug address and smartplugctl to send command to it (on/off/read status). 12 | This 2 scripts use SmartPlugSmpB16 python module for manage Bluetooth Low 13 | Energy exchanges. 14 | 15 | An older release of smartplugctl use bluez gatttool commmand line utility to do 16 | the stuff. In this script bluepy module is not require, it is available under 17 | old_scripts/. 18 | 19 | ### Require 20 | 21 | Python module bluepy (https://github.com/IanHarvey/bluepy) is require. You can 22 | install it with : 23 | 24 | sudo apt-get install -y python-pip libglib2.0-dev 25 | sudo pip install bluepy 26 | 27 | ### Setup 28 | 29 | sudo apt-get install -y python-setuptools 30 | git clone https://github.com/sourceperl/smartplugctl.git 31 | cd smartplugctl 32 | sudo python setup.py install 33 | 34 | ### Find a plug 35 | 36 | sudo smartplugscan 37 | 38 | ### Turn plug on 39 | 40 | smartplugctl 98:7B:F3:34:78:52 on 41 | 42 | ### Turn plug off 43 | 44 | smartplugctl 98:7B:F3:34:78:52 off 45 | 46 | ### Read plug status (on/off, power level and grid voltage) 47 | 48 | smartplugctl 98:7B:F3:34:78:52 status 49 | 50 | ### Read hourly consumption history (24 hours from now) 51 | 52 | smartplugctl 98:7B:F3:34:78:52 history_hour 53 | 54 | ### Read daily consumption history (30 days from today) 55 | 56 | smartplugctl 98:7B:F3:34:78:52 history_day 57 | 58 | ### Manage plug LED (usefull for bedroom) 59 | 60 | Turn off: 61 | 62 | smartplugctl 98:7b:f3:34:78:52 light_enable off 63 | 64 | Turn on: 65 | 66 | smartplugctl 98:7b:f3:34:78:52 light_enable on 67 | 68 | ### Set plug date/time 69 | 70 | smartplugctl 98:7B:F3:34:78:52 set_time 71 | 72 | This is required for good plug program schedule. 73 | 74 | ### Update a program 75 | 76 | smartplugctl 98:7B:F3:34:78:52 program_update 0 - 13:23 77 | 78 | Update first program for no switch on and switch off at 13h23. 79 | 80 | smartplugctl 98:7B:F3:34:78:52 program_update 1 10:10 13:23 81 | 82 | Update second program for switch on at 10h10 and switch off at 13h23. 83 | 84 | ### Enable / disable a program 85 | 86 | smartplugctl 98:7B:F3:34:78:52 program_enable 0 on 87 | 88 | Enable first program. 89 | 90 | smartplugctl 98:7B:F3:34:78:52 program_enable 1 on 91 | 92 | Disable second program. 93 | 94 | ### Delete a program 95 | 96 | smartplugctl 98:7B:F3:34:78:52 program_delete 0 97 | 98 | ### Add program example 99 | 100 | Set plug on at 10:00 and off at 10:10 : 101 | 102 | smartplugctl 98:7B:F3:34:78:52 set_time 103 | smartplugctl 98:7B:F3:34:78:52 program_update 0 10:00 10:10 104 | smartplugctl 98:7B:F3:34:78:52 program_enable 0 on 105 | 106 | Check program list to ensure all is ok : 107 | 108 | smartplugctl 98:7B:F3:34:78:52 program_read 109 | 110 | ### Help 111 | 112 | smartplugctl -h 113 | 114 | ## Python module 115 | 116 | Alternatively to smartplug scripts you can directly use module from python code. 117 | 118 | ### Usage example 119 | 120 | # cycle power then log plug state and power level to terminal 121 | import pySmartPlugSmpB16 122 | import time 123 | 124 | # connect to the plug with bluetooth address 125 | plug = pySmartPlugSmpB16.SmartPlug('98:7B:F3:34:78:52') 126 | 127 | # cycle power 128 | plug.off() 129 | time.sleep(2.0) 130 | plug.on() 131 | 132 | # display state and power level 133 | while True: 134 | (state, power, voltage) = plug.status_request() 135 | print('plug state = %s' % ('on' if state else 'off')) 136 | print('plug power = %d W' % power) 137 | print('plug voltage = %d V' % voltage) 138 | time.sleep(2.0) 139 | -------------------------------------------------------------------------------- /img/rpi_smartplug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourceperl/smartplugctl/365fc2d88e4d49ea2666656ade8999cd6ef604d3/img/rpi_smartplug.jpg -------------------------------------------------------------------------------- /old_scripts/smartplugctl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # smartPlug AWOX control with Bluez 4 | # 5 | # Control an AWOX smartPlug (BLE electrical plug with relay) from command line 6 | # sample: './smart_plug_ctl.py 98:7B:F3:34:78:52 on' to turn on the plug 7 | # 8 | # needs: bluez installed with gatttool utility 9 | # 10 | # license: MIT 11 | 12 | from __future__ import print_function 13 | import sys 14 | import time 15 | import pexpect 16 | import argparse 17 | 18 | # some const 19 | RET_CODE_OK = 0 20 | RET_CODE_ERROR = 1 21 | 22 | # some var 23 | return_code = RET_CODE_OK 24 | 25 | # parse args 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('ble_addr', type=str, 28 | help='plug bluetooth LE address (like 98:7b:f3:34:78:52)') 29 | parser.add_argument('command', type=str, choices=['on', 'off', 'status'], 30 | help='command to send at plug') 31 | args = parser.parse_args() 32 | 33 | # format gatttool commands 34 | cmd_prefix = 'gatttool -b {0}'.format(args.ble_addr) 35 | cmd_on = cmd_prefix + ' --char-write -a 0x2b -n 0f06030001000005ffff' 36 | cmd_off = cmd_prefix + ' --char-write -a 0x2b -n 0f06030000000004ffff' 37 | cmd_status = cmd_prefix + ' --handle=0x2b --char-write-req --value=0f050400000005ffff --listen' 38 | 39 | # set plug on/off 40 | if args.command == 'on' or args.command == 'off': 41 | do_retry = 3 42 | while True: 43 | cmd_state = cmd_on if args.command == 'on' else cmd_off 44 | (command_output, command_code) = pexpect.run(cmd_state, withexitstatus=1) 45 | return_code = RET_CODE_OK if command_code == 0 else RET_CODE_ERROR 46 | do_retry -= 1 47 | # exit if cmd is ok or too retry 48 | if command_code == 0 or do_retry < 1: 49 | break 50 | else: 51 | print('error, do another try', file=sys.stderr) 52 | # wait before next try 53 | time.sleep(0.2) 54 | 55 | # print status 56 | if return_code == RET_CODE_OK: 57 | print('smartPlug is set {0}'.format(args.command)) 58 | else: 59 | print('unable to contact smartPlug', file=sys.stderr) 60 | 61 | # request plug power level and state 62 | elif args.command == 'status': 63 | # launch command and wait notification 64 | p = pexpect.spawn(cmd_status) 65 | i = p.expect([pexpect.TIMEOUT, pexpect.EOF, r'0f 0f.*ff ff'], timeout=10.0) 66 | p.terminate() 67 | 68 | # print status 69 | if i == 0 or i == 1: 70 | print('error occur (gatttool say = \'%s\')' % p.before.rstrip(), file=sys.stderr) 71 | return_code = RET_CODE_ERROR 72 | elif i == 2: 73 | # decode data (plug state at byte index 4, power in mW at bytes index 6 to 9) 74 | data = p.after.split() 75 | # print('RAW = %s' % data) 76 | status = 'on' if int(data[4], 16) == 1 else 'off' 77 | power = int(data[6]+data[7]+data[8]+data[9], 16) / 1000 78 | # print result 79 | print('plug state = %s' % status) 80 | print('plug power = %d W' % power) 81 | return_code = RET_CODE_OK 82 | 83 | # return error code 84 | sys.exit(return_code) 85 | -------------------------------------------------------------------------------- /pySmartPlugSmpB16.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import datetime 3 | import struct 4 | import sys 5 | import array 6 | from bluepy import btle 7 | 8 | START_OF_MESSAGE = b'\x0f' 9 | END_OF_MESSAGE = b'\xff\xff' 10 | 11 | 12 | class SmartPlug(btle.Peripheral): 13 | def __init__(self, addr): 14 | btle.Peripheral.__init__(self, addr) 15 | self.delegate = NotificationDelegate() 16 | self.setDelegate(self.delegate) 17 | self.plug_svc = self.getServiceByUUID('0000fff0-0000-1000-8000-00805f9b34fb') 18 | self.plug_cmd_ch = self.plug_svc.getCharacteristics('0000fff3-0000-1000-8000-00805f9b34fb')[0] 19 | self.plug_name_ch = self.plug_svc.getCharacteristics('0000fff6-0000-1000-8000-00805f9b34fb')[0] 20 | 21 | def get_name(self): 22 | name = self.plug_name_ch.read() 23 | return name.decode('iso-8859-1') 24 | 25 | def set_time(self): 26 | self.delegate.chg_is_ok = False 27 | buffer = b'\x01\x00' 28 | now = datetime.datetime.now() 29 | buffer += struct.pack(">BBBBBH", now.second, now.minute, now.hour, now.day, now.month, now.year) 30 | buffer += b'\x00\x00\x00\x00' 31 | self.write_data(self.get_buffer(buffer)) 32 | self.wait_data(0.5) 33 | return self.delegate.chg_is_ok 34 | 35 | def set_name(self, name): 36 | buffer = b'\x02\x00' 37 | buffer += struct.pack(">20s", name.encode('iso-8859-1')) 38 | self.write_data(self.get_buffer(buffer)) 39 | self.wait_data(0.5) 40 | return self.delegate.chg_is_ok 41 | 42 | def on(self): 43 | self.delegate.chg_is_ok = False 44 | self.write_data(self.get_buffer(binascii.unhexlify('0300010000'))) 45 | self.wait_data(0.5) 46 | return self.delegate.chg_is_ok 47 | 48 | def off(self): 49 | self.delegate.chg_is_ok = False 50 | self.write_data(self.get_buffer(binascii.unhexlify('0300000000'))) 51 | self.wait_data(0.5) 52 | return self.delegate.chg_is_ok 53 | 54 | def status_request(self): 55 | self.write_data(self.get_buffer(binascii.unhexlify('04000000'))) 56 | self.wait_data(2.0) 57 | return self.delegate.state, self.delegate.power, self.delegate.voltage 58 | 59 | def power_history_hour_request(self): 60 | self.write_data(self.get_buffer(binascii.unhexlify('0a000000'))) 61 | self.wait_data(2.0) 62 | return self.delegate.history 63 | 64 | def power_history_day_request(self): 65 | self.write_data(self.get_buffer(binascii.unhexlify('0b000000'))) 66 | self.wait_data(2.0) 67 | return self.delegate.history 68 | 69 | def program_write(self, program_list): 70 | buffer = b'\x06\x00' 71 | for program in program_list: 72 | start_hour = -1 73 | start_minute = -1 74 | if program["start"]: 75 | start_hour, start_minute = map(int, program["start"].split(':')) 76 | end_hour = -1 77 | end_minute = -1 78 | if program["end"]: 79 | end_hour, end_minute = map(int, program["end"].split(':')) 80 | 81 | buffer += struct.pack(">?16sBbbbb", True, program["name"].encode('iso-8859-1'), program["flags"], start_hour, start_minute, end_hour, end_minute) 82 | 83 | buffer = buffer.ljust(2 + 5*22, '\0') 84 | self.write_data(self.get_buffer(buffer)) 85 | self.wait_data(0.5) 86 | return self.delegate.history 87 | 88 | def reset(self): 89 | self.delegate.chg_is_ok = False 90 | self.write_data(self.get_buffer(binascii.unhexlify('0F00000000'))) 91 | self.wait_data(0.5) 92 | return self.delegate.chg_is_ok 93 | 94 | def light_enable(self, enable): 95 | self.delegate.chg_is_ok = False 96 | buffer = b'\x0F\x00\x01' 97 | buffer += struct.pack(">?x",enable) 98 | self.write_data(self.get_buffer(buffer)) 99 | self.wait_data(0.5) 100 | return self.delegate.chg_is_ok 101 | 102 | def program_request(self): 103 | self.write_data(self.get_buffer(binascii.unhexlify('07000000'))) 104 | self.wait_data(2.0) 105 | return self.delegate.programs 106 | 107 | def calculate_checksum(self, message): 108 | return (sum(bytearray(message)) + 1) & 0xff 109 | 110 | def get_buffer(self, message): 111 | return START_OF_MESSAGE + struct.pack("B",len(message) + 1) + message + struct.pack("B",self.calculate_checksum(message)) + END_OF_MESSAGE 112 | 113 | def write_data(self, data): 114 | remaining_data = data 115 | while len(remaining_data) > 0: 116 | self.plug_cmd_ch.write(remaining_data[:20]) 117 | remaining_data = remaining_data[20:] 118 | 119 | def wait_data(self, timeout): 120 | self.delegate.need_data = True 121 | while self.delegate.need_data and self.waitForNotifications(timeout): 122 | pass 123 | 124 | 125 | class NotificationDelegate(btle.DefaultDelegate): 126 | def __init__(self): 127 | btle.DefaultDelegate.__init__(self) 128 | self.state = False 129 | self.power = 0 130 | self.voltage = 0 131 | self.chg_is_ok = False 132 | self.history = [] 133 | self.programs = [] 134 | self._buffer = b'' 135 | self.need_data = True 136 | 137 | def handleNotification(self, cHandle, data): 138 | # not sure 0x0f indicate begin of buffer but 139 | if data[:1] == START_OF_MESSAGE: 140 | self._buffer = data 141 | else: 142 | self._buffer = self._buffer + data 143 | if self._buffer[-2:] == END_OF_MESSAGE: 144 | self.handle_data(self._buffer) 145 | self._buffer = b'' 146 | self.need_data = False 147 | 148 | def handle_data(self, bytes_data): 149 | # it's a set time confirm notification ? 150 | if bytes_data[0:5] == b'\x0f\x04\x01\x00\x00': 151 | self.chg_is_ok = True 152 | # it's a set name confirm notification ? 153 | if bytes_data[0:5] == b'\x0f\x04\x02\x00\x00': 154 | self.chg_is_ok = True 155 | # it's a state change confirm notification ? 156 | if bytes_data[0:3] == b'\x0f\x04\x03': 157 | self.chg_is_ok = True 158 | # it's a state/power notification ? 159 | if bytes_data[0:3] == b'\x0f\x0f\x04': 160 | (state, dummy, power, voltage) = struct.unpack_from(">?BIB", bytes_data, offset=4) 161 | self.state = state 162 | self.power = power / 1000 163 | self.voltage = voltage 164 | # it's a power history for last 24h notif ? 165 | if bytes_data[0:3] == b'\x0f\x33\x0a': 166 | history_array = array.array('H', bytes_data[4:52]) 167 | # get the right byte order 168 | if sys.byteorder == 'little': 169 | history_array.byteswap() 170 | self.history = reversed(history_array.tolist()) 171 | # it's a power history kWh/day notif ? 172 | if bytes_data[0:3] == b'\x0f\x7b\x0b': 173 | history_array = array.array('I', bytes_data[4:124]) 174 | # get the right byte order 175 | if sys.byteorder == 'little': 176 | history_array.byteswap() 177 | self.history = reversed(history_array.tolist()) 178 | # it's a programs notif ? 179 | if bytes_data[0:3] == b'\x0f\x71\x07': 180 | program_offset = 4 181 | self.programs = [] 182 | while program_offset + 21 < len(bytes_data): 183 | (present, name, flags, start_hour, start_minute, end_hour, end_minute) = struct.unpack_from(">?16sBbbbb", bytes_data, program_offset) 184 | #TODO interpret flags (day of program ?) 185 | if present: 186 | start_time = None 187 | end_time = None 188 | if start_hour >= 0 and start_minute >= 0: 189 | start_time = "{0:02d}:{1:02d}".format(start_hour, start_minute) 190 | if end_hour >= 0 and end_minute >= 0: 191 | end_time = "{0:02d}:{1:02d}".format(end_hour, end_minute) 192 | self.programs.append({"name" : name.decode('iso-8859-1').strip('\0'), "flags":flags, "start":start_time, "end":end_time}) 193 | program_offset += 22 194 | if bytes_data[0:4] == b'\x0f\x05\x0f\x00': 195 | self.chg_is_ok = True 196 | # SmartPlugSmpB16 usage sample: cycle power then log plug state and power level to terminal 197 | if __name__ == '__main__': 198 | import time 199 | 200 | # connect to the plug with bluetooth address 201 | plug = SmartPlug('98:7B:F3:34:78:52') 202 | 203 | # cycle power 204 | plug.off() 205 | time.sleep(2.0) 206 | plug.on() 207 | 208 | # display state and power level 209 | while True: 210 | (state, power, voltage) = plug.status_request() 211 | print('plug state = %s' % ('on' if state else 'off')) 212 | print('plug power = %d W' % power) 213 | time.sleep(2.0) 214 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bluepy >= 1.0.5 2 | -------------------------------------------------------------------------------- /scripts/smartplugctl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # smartPlug AWOX control with Bluez 4 | # 5 | # Control an AWOX smartPlug (BLE electrical plug with relay) from command line 6 | # sample: './smart_plug_ctl.py 98:7B:F3:34:78:52 on' to turn on the plug 7 | # 8 | # needs: bluez and python bluepy module 9 | # 10 | # license: MIT 11 | 12 | from __future__ import print_function 13 | import sys 14 | import argparse 15 | import pprint 16 | from pySmartPlugSmpB16 import SmartPlug, btle 17 | 18 | # parse args 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument('ble_addr', type=str, 21 | help='plug bluetooth LE address (like 98:7b:f3:34:78:52)') 22 | subparsers = parser.add_subparsers(title='command', dest='command') 23 | subparsers.add_parser('on', help='Turn plug on') 24 | subparsers.add_parser('off', help='Turn plug off') 25 | subparsers.add_parser('status', help='Read plug status (on/off, power level and grid voltage)') 26 | subparsers.add_parser('history_hour', help='Read hourly consumption history (24 hours from now)') 27 | subparsers.add_parser('history_day', help='Read daily consumption history (30 days from today)') 28 | subparsers.add_parser('program_read', help='Read current programs') 29 | parser_program_enable = subparsers.add_parser('program_enable', help='Enable/Disable one program') 30 | parser_program_enable.add_argument('program', type=int, choices=range(0, 5), default=0, help='Program number') 31 | parser_program_enable.add_argument('state', type=str, choices=['on', 'off']) 32 | parser_program_update = subparsers.add_parser('program_update', help='Create/modify one program') 33 | parser_program_update.add_argument('program', type=int, choices=range(0, 5), default=0, help='Program number') 34 | parser_program_update.add_argument('start', type=str, default='-', help='Start time or - for no value') 35 | parser_program_update.add_argument('end', type=str, default='-', help='End time or - for no value') 36 | parser_program_delete = subparsers.add_parser('program_delete', help='Delete one program') 37 | parser_program_delete.add_argument('program', type=int, choices=range(0, 5), default=0, help='Program number') 38 | subparsers.add_parser('set_time', help='Set current time to the plug') 39 | subparsers.add_parser('reset', help='Reset the plug history and program') 40 | parser_light_enable = subparsers.add_parser('light_enable', help='Enable/Disable notification light') 41 | parser_light_enable.add_argument('state', type=str, choices=['on', 'off']) 42 | subparsers.add_parser('get_name', help='Get the plug name') 43 | parser_set_name = subparsers.add_parser('set_name', help='Set the plug name') 44 | parser_set_name.add_argument('name', type=str) 45 | 46 | args = parser.parse_args() 47 | 48 | # connect to the plug (BLE connect) 49 | try: 50 | plug = SmartPlug(args.ble_addr) 51 | except btle.BTLEException as err: 52 | sys.exit('error when connect to %s (code %d)' % (args.ble_addr, err.code)) 53 | 54 | # set plug on/off 55 | if args.command == 'on': 56 | try: 57 | is_ok = plug.on() 58 | except btle.BTLEException as err: 59 | sys.exit('error when setting plug %s on (code %d)' % (args.ble_addr, err.code)) 60 | if is_ok: 61 | print('smartPlug is set on') 62 | else: 63 | sys.exit('unable to set smartPlug on') 64 | elif args.command == 'off': 65 | try: 66 | is_ok = plug.off() 67 | except btle.BTLEException as err: 68 | sys.exit('error when setting plug %s off (code %d)' % (args.ble_addr, err.code)) 69 | if is_ok: 70 | print('smartPlug is set off') 71 | else: 72 | sys.exit('unable to set smartPlug off') 73 | elif args.command == 'status': 74 | try: 75 | (state, power, voltage) = plug.status_request() 76 | except btle.BTLEException as err: 77 | sys.exit('error when requesting stat to plug %s (code %d)' % (args.ble_addr, err.code)) 78 | # print result 79 | status = 'on' if state else 'off' 80 | print('plug state = %s' % status) 81 | print('plug power = %d W' % power) 82 | print('plug voltage = %d V' % voltage) 83 | elif args.command == 'program_read': 84 | try: 85 | (programs) = plug.program_request() 86 | except btle.BTLEException as err: 87 | sys.exit('error when requesting program to plug %s (code %d)' % (args.ble_addr, err.code)) 88 | # print result 89 | print('plug programs : ' ) 90 | pprint.pprint(programs) 91 | elif args.command == 'history_hour': 92 | try: 93 | history = plug.power_history_hour_request() 94 | except btle.BTLEException as err: 95 | sys.exit('error when requesting power history to plug %s (code %d)' % (args.ble_addr, err.code)) 96 | # print result 97 | for i, h in enumerate(history): 98 | print('plug power h-%02u = %d Wh' % (i+1, h)) 99 | elif args.command == 'history_day': 100 | try: 101 | history = plug.power_history_day_request() 102 | except btle.BTLEException as err: 103 | sys.exit('error when requesting power history to plug %s (code %d)' % (args.ble_addr, err.code)) 104 | # print result 105 | for i, h in enumerate(history): 106 | print('plug power j-%02u = %d Wh' % (i+1, h)) 107 | elif args.command == 'program_enable': 108 | try: 109 | (programs) = plug.program_request() 110 | except btle.BTLEException as err: 111 | sys.exit('error when requesting program to plug %s (code %d)' % (args.ble_addr, err.code)) 112 | program_number = args.program 113 | if len(programs) > program_number: 114 | if args.state == "on": 115 | programs[program_number]["flags"] |= 0x80 116 | else: 117 | programs[program_number]["flags"] &= 0x7F 118 | else: 119 | sys.exit('program %d does not exist' % (program_number)) 120 | try: 121 | plug.program_write(programs) 122 | except btle.BTLEException as err: 123 | sys.exit('error when writing program to plug %s (code %d)' % (args.ble_addr, err.code)) 124 | elif args.command == 'program_update': 125 | try: 126 | programs = plug.program_request() 127 | except btle.BTLEException as err: 128 | sys.exit('error when requesting program to plug %s (code %d)' % (args.ble_addr, err.code)) 129 | program_number = args.program 130 | 131 | # add programs empty to go to index 132 | while len(programs) <= program_number: 133 | programs.append({'name':'program', 'start':None, 'end':None, 'flags':0x7F}) 134 | 135 | if args.start == '-': 136 | programs[program_number]["start"] = None 137 | else: 138 | programs[program_number]["start"] = args.start 139 | if args.end == '-': 140 | programs[program_number]["end"] = None 141 | else: 142 | programs[program_number]["end"] = args.end 143 | try: 144 | plug.program_write(programs) 145 | except btle.BTLEException as err: 146 | sys.exit('error when writing program to plug %s (code %d)' % (args.ble_addr, err.code)) 147 | elif args.command == 'program_delete': 148 | try: 149 | programs = plug.program_request() 150 | except btle.BTLEException as err: 151 | sys.exit('error when requesting program to plug %s (code %d)' % (args.ble_addr, err.code)) 152 | program_number = args.program 153 | 154 | if len(programs) > program_number: 155 | programs.pop(program_number) 156 | else: 157 | sys.exit('program %d does not exist' % (program_number)) 158 | 159 | try: 160 | plug.program_write(programs) 161 | except btle.BTLEException as err: 162 | sys.exit('error when writing program to plug %s (code %d)' % (args.ble_addr, err.code)) 163 | elif args.command == 'set_time': 164 | try: 165 | is_ok = plug.set_time() 166 | except btle.BTLEException as err: 167 | sys.exit('error when setting time to plug %s (code %d)' % (args.ble_addr, err.code)) 168 | if is_ok: 169 | print('time is set on smartPlug') 170 | else: 171 | sys.exit('unable to set time on SmartPlug') 172 | elif args.command == 'reset': 173 | try: 174 | is_ok = plug.reset() 175 | except btle.BTLEException as err: 176 | sys.exit('error when reset plug %s (code %d)' % (args.ble_addr, err.code)) 177 | if is_ok: 178 | print('reset smartPlug') 179 | else: 180 | sys.exit('unable reset SmartPlug') 181 | elif args.command == 'light_enable': 182 | try: 183 | is_ok = plug.light_enable(args.state == 'on') 184 | except btle.BTLEException as err: 185 | sys.exit('error when setting light enable to plug %s (code %d)' % (args.ble_addr, err.code)) 186 | if is_ok: 187 | print('light enable set on smartPlug') 188 | else: 189 | sys.exit('unable to set light enable on SmartPlug') 190 | elif args.command == 'get_name': 191 | try: 192 | name = plug.get_name() 193 | except btle.BTLEException as err: 194 | sys.exit('error when requesting name of plug %s (code %d)' % (args.ble_addr, err.code)) 195 | print('plug name = %s' % name) 196 | elif args.command == 'set_name': 197 | try: 198 | is_ok = plug.set_name(args.name) 199 | except btle.BTLEException as err: 200 | sys.exit('error when setting name to plug %s (code %d)' % (args.ble_addr, err.code)) 201 | if is_ok: 202 | print('name set on smartPlug') 203 | else: 204 | sys.exit('unable to set name on SmartPlug') 205 | # disconnect BLE 206 | plug.disconnect() 207 | 208 | # exit without error 209 | sys.exit(0) 210 | 211 | -------------------------------------------------------------------------------- /scripts/smartplugscan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # smartPlug AWOX control scanner 4 | # 5 | # Search AWOX smartPlug (BLE electrical plug with relay) from command line 6 | # 7 | # sample: 8 | # lefebvre@debian:~$ sudo smartplugscan 9 | # 98:7b:f3:34:78:52, -82 dBm, SMP-B16-FR 10 | # 11 | # needs: bluez and python bluepy module 12 | # 13 | # license: MIT 14 | 15 | from __future__ import print_function 16 | import os 17 | import sys 18 | from bluepy.btle import Scanner, DefaultDelegate 19 | 20 | 21 | class ScanDelegate(DefaultDelegate): 22 | def __init__(self): 23 | DefaultDelegate.__init__(self) 24 | 25 | def handleDiscovery(self, dev, isNewDev, isNewData): 26 | if isNewDev: 27 | for (ad_type, desc, value) in dev.getScanData(): 28 | if ad_type == 9 and value.startswith("SMP-B16-"): 29 | print("%s, %d dBm, %s" % (dev.addr, dev.rssi, value)) 30 | 31 | 32 | # root is need for doing a BLE scan 33 | if os.geteuid() != 0: 34 | sys.exit('BLE scan need to be run by root') 35 | 36 | # start a BLE scan for 2s 37 | scanner = Scanner().withDelegate(ScanDelegate()) 38 | devices = scanner.scan(2.0) 39 | 40 | # exit with no error 41 | sys.exit(0) 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('requirements.txt') as f: 4 | required = f.read().splitlines() 5 | 6 | setup( 7 | name='smartplugctl', 8 | version='0.0.5', 9 | license='MIT', 10 | url='https://github.com/sourceperl/smartplugctl', 11 | platforms='any', 12 | install_requires=required, 13 | py_modules=[ 14 | 'pySmartPlugSmpB16' 15 | ], 16 | scripts=[ 17 | 'scripts/smartplugctl', 18 | 'scripts/smartplugscan' 19 | ] 20 | ) 21 | --------------------------------------------------------------------------------