about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md37
-rw-r--r--broadlink/__init__.py396
-rw-r--r--cli/README.md7
-rwxr-xr-xcli/broadlink_cli64
-rwxr-xr-xcli/broadlink_discovery8
-rw-r--r--requirements.txt2
-rw-r--r--setup.py9
7 files changed, 420 insertions, 103 deletions
diff --git a/README.md b/README.md
index 48708827ce36..d5e0154db221 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,13 @@
 Python control for Broadlink RM2 IR controllers
 ===============================================
 
-A simple Python API for controlling IR controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, only RM Pro (referred to as RM2 in the codebase) and A1 sensor platform devices are supported. There is currently no support for the cloud API.
+A simple Python API for controlling IR controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, the following devices are currently supported:
+
+* RM Pro (referred to as RM2 in the codebase)
+* A1 sensor platform devices are supported
+* RM3 mini IR blaster
+
+There is currently no support for the cloud API.
 
 Example use
 -----------
@@ -42,6 +48,10 @@ Sweep RF frequencies:
 devices[0].sweep_frequency()
 ```
 
+Cancel sweep RF frequencies:
+```
+devices[0].cancel_sweep_frequency()
+```
 Check whether a frequency has been found:
 ```
 found = devices[0].check_frequency()
@@ -85,6 +95,11 @@ Check power state on a SmartPlug:
 state = devices[0].check_power()
 ```
 
+Check energy consumption on a SmartPlug:
+```
+state = devices[0].get_energy()
+```
+
 Set power state for S1 on a SmartPowerStrip MP1:
 ```
 devices[0].set_power(1, True)
@@ -94,23 +109,3 @@ Check power state on a SmartPowerStrip:
 ```
 state = devices[0].check_power()
 ```
-
-Learning RF packets
--------------------
-
-timeout = 10
-devices[0].sweep_frequency()
-# Hold down the rf button
-for i in range(0, timeout):
-  found = devices[0].check_frequency()
-  if found == True:
-    break
-  time.sleep(1)
-# Tap the rf button
-for i in range(0, timeout):
-  found = devices[0].find_rf_packet()
-  if found == True:
-    break
-  time.sleep(1)
-# Obtain the code
-code = devices[0].check_data()
diff --git a/broadlink/__init__.py b/broadlink/__init__.py
index 8a193d556101..33bf5e8f879b 100644
--- a/broadlink/__init__.py
+++ b/broadlink/__init__.py
@@ -13,53 +13,50 @@ import sys
 import threading
 import codecs
 
+
 def gendevice(devtype, host, mac):
-  if devtype == 0: # SP1
-    return sp1(host=host, mac=mac)
-  if devtype == 0x2711: # SP2
-    return sp2(host=host, mac=mac)
-  if devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2
-    return sp2(host=host, mac=mac)
-  if devtype == 0x2720: # SPMini
-    return sp2(host=host, mac=mac)
-  elif devtype == 0x753e: # SP3
-    return sp2(host=host, mac=mac)
-  elif devtype == 0x947a or devtype == 0x9479: # SP3S
-    return sp2(host=host, mac=mac)
-  elif devtype == 0x2728: # SPMini2
-    return sp2(host=host, mac=mac)
-  elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini
-    return sp2(host=host, mac=mac)
-  elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2
-    return sp2(host=host, mac=mac)
-  elif devtype == 0x2736: # SPMiniPlus
-    return sp2(host=host, mac=mac)
-  elif devtype == 0x2712: # RM2
-    return rm(host=host, mac=mac)
-  elif devtype == 0x2737: # RM Mini
-    return rm(host=host, mac=mac)
-  elif devtype == 0x273d: # RM Pro Phicomm
-    return rm(host=host, mac=mac)
-  elif devtype == 0x2783: # RM2 Home Plus
-    return rm(host=host, mac=mac)
-  elif devtype == 0x277c: # RM2 Home Plus GDT
-    return rm(host=host, mac=mac)
-  elif devtype == 0x272a: # RM2 Pro Plus
-    return rm(host=host, mac=mac)
-  elif devtype == 0x2787: # RM2 Pro Plus2
-    return rm(host=host, mac=mac)
-  elif devtype == 0x278b: # RM2 Pro Plus BL
-    return rm(host=host, mac=mac)
-  elif devtype == 0x278f: # RM Mini Shate
-    return rm(host=host, mac=mac)
-  elif devtype == 0x2714: # A1
-    return a1(host=host, mac=mac)
-  elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7
-    return mp1(host=host, mac=mac)
-  elif devtype == 0x2722: # S1 (SmartOne Alarm Kit)
-    return S1C(host=host, mac=mac)
-  else:
-    return device(host=host, mac=mac)
+  devices = {
+          sp1: [0],
+          sp2: [0x2711,                          # SP2
+                0x2719, 0x7919, 0x271a, 0x791a,  # Honeywell SP2
+                0x2720,                          # SPMini
+                0x753e,                          # SP3
+                0x7D00,                          # OEM branded SP3
+                0x947a, 0x9479,                  # SP3S
+                0x2728,                          # SPMini2
+                0x2733, 0x273e,                  # OEM branded SPMini
+                0x7530, 0x7918,                  # OEM branded SPMini2
+                0x2736                           # SPMiniPlus
+                ],
+          rm: [0x2712,  # RM2
+               0x2737,  # RM Mini
+               0x273d,  # RM Pro Phicomm
+               0x2783,  # RM2 Home Plus
+               0x277c,  # RM2 Home Plus GDT
+               0x272a,  # RM2 Pro Plus
+               0x2787,  # RM2 Pro Plus2
+               0x279d,  # RM2 Pro Plus3
+               0x27a9,  # RM2 Pro Plus_300
+               0x278b,  # RM2 Pro Plus BL
+               0x2797,  # RM2 Pro Plus HYC
+               0x27a1,  # RM2 Pro Plus R1
+               0x27a6,  # RM2 Pro PP
+               0x278f   # RM Mini Shate
+               ],
+          a1: [0x2714],  # A1
+          mp1: [0x4EB5,  # MP1
+                0x4EF7   # Honyar oem mp1
+                ],
+          hysen: [0x4EAD],  # Hysen controller
+          S1C: [0x2722],  # S1 (SmartOne Alarm Kit)
+          dooya: [0x4E4D]  # Dooya DT360E (DOOYA_CURTAIN_V2)
+          }
+
+  # Look for the class associated to devtype in devices
+  [deviceClass] = [dev for dev in devices if devtype in devices[dev]] or [None]
+  if deviceClass is None:
+    return device(host=host, mac=mac, devtype=devtype)
+  return deviceClass(host=host, mac=mac, devtype=devtype)
 
 def discover(timeout=None, local_ip_address=None):
   if local_ip_address is None:
@@ -122,6 +119,8 @@ def discover(timeout=None, local_ip_address=None):
     host = response[1]
     mac = responsepacket[0x3a:0x40]
     devtype = responsepacket[0x34] | responsepacket[0x35] << 8
+
+
     return gendevice(devtype, host, mac)
   else:
     while (time.time() - starttime) < timeout:
@@ -139,10 +138,12 @@ def discover(timeout=None, local_ip_address=None):
     return devices
 
 
+
 class device:
-  def __init__(self, host, mac, timeout=10):
+  def __init__(self, host, mac, devtype, timeout=10):
     self.host = host
     self.mac = mac
+    self.devtype = devtype
     self.timeout = timeout
     self.count = random.randrange(0xffff)
     self.key = bytearray([0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02])
@@ -155,7 +156,7 @@ class device:
     self.type = "Unknown"
     self.lock = threading.Lock()
 
-    if 'pyaes' in sys.modules:
+    if 'pyaes' in globals():
         self.encrypt = self.encrypt_pyaes
         self.decrypt = self.decrypt_pyaes
     else:
@@ -164,11 +165,11 @@ class device:
 
   def encrypt_pyaes(self, payload):
     aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv))
-    return "".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)])
+    return b"".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)])
 
   def decrypt_pyaes(self, payload):
     aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv))
-    return "".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)])
+    return b"".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)])
 
   def encrypt_pycrypto(self, payload):
     aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv))
@@ -218,6 +219,7 @@ class device:
 
     self.id = payload[0x00:0x04]
     self.key = key
+
     return True
 
   def get_type(self):
@@ -253,7 +255,7 @@ class device:
     # pad the payload for AES encryption
     if len(payload)>0:
       numpad=(len(payload)//16+1)*16
-      payload=payload.ljust(numpad,b"\x00")
+      payload=payload.ljust(numpad, b"\x00")
 
     checksum = 0xbeaf
     for i in range(len(payload)):
@@ -290,8 +292,8 @@ class device:
 
 
 class mp1(device):
-  def __init__ (self, host, mac):
-    device.__init__(self, host, mac)
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
     self.type = "MP1"
 
   def set_power_mask(self, sid_mask, state):
@@ -353,8 +355,8 @@ class mp1(device):
 
 
 class sp1(device):
-  def __init__ (self, host, mac):
-    device.__init__(self, host, mac)
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
     self.type = "SP1"
 
   def set_power(self, state):
@@ -364,15 +366,28 @@ class sp1(device):
 
 
 class sp2(device):
-  def __init__ (self, host, mac):
-    device.__init__(self, host, mac)
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
     self.type = "SP2"
 
   def set_power(self, state):
     """Sets the power state of the smart plug."""
     packet = bytearray(16)
     packet[0] = 2
-    packet[4] = 1 if state else 0
+    if self.check_nightlight():
+      packet[4] = 3 if state else 2
+    else:
+      packet[4] = 1 if state else 0
+    self.send_packet(0x6a, packet)
+
+  def set_nightlight(self, state):
+    """Sets the night light state of the smart plug"""
+    packet = bytearray(16)
+    packet[0] = 2
+    if self.check_power():
+      packet[4] = 3 if state else 1
+    else:
+      packet[4] = 2 if state else 0
     self.send_packet(0x6a, packet)
 
   def check_power(self):
@@ -384,9 +399,35 @@ class sp2(device):
     if err == 0:
       payload = self.decrypt(bytes(response[0x38:]))
       if type(payload[0x4]) == int:
-        state = bool(payload[0x4])
+        if payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD:
+          state = True
+        else:
+          state = False
       else:
-        state = bool(ord(payload[0x4]))
+        if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD:
+          state = True
+        else:
+          state = False
+      return state
+
+  def check_nightlight(self):
+    """Returns the power state of the smart plug."""
+    packet = bytearray(16)
+    packet[0] = 1
+    response = self.send_packet(0x6a, packet)
+    err = response[0x22] | (response[0x23] << 8)
+    if err == 0:
+      payload = self.decrypt(bytes(response[0x38:]))
+      if type(payload[0x4]) == int:
+        if payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF:
+          state = True
+        else:
+          state = False
+      else:
+        if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF:
+          state = True
+        else:
+          state = False
       return state
 
   def get_energy(self):
@@ -395,13 +436,16 @@ class sp2(device):
     err = response[0x22] | (response[0x23] << 8)
     if err == 0:
       payload = self.decrypt(bytes(response[0x38:]))
-      energy = int(hex(ord(payload[7]) * 256 + ord(payload[6]))[2:]) + int(hex(ord(payload[5]))[2:])/100.0
+      if type(payload[0x07]) == int:
+        energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:])/100.0
+      else:
+        energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int(hex(ord(payload[0x05]))[2:])/100.0
       return energy
 
 
 class a1(device):
-  def __init__ (self, host, mac):
-    device.__init__(self, host, mac)
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
     self.type = "A1"
 
   def check_sensors(self):
@@ -478,8 +522,8 @@ class a1(device):
 
 
 class rm(device):
-  def __init__ (self, host, mac):
-    device.__init__(self, host, mac)
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
     self.type = "RM2"
 
   def check_data(self):
@@ -546,10 +590,11 @@ class rm(device):
         temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0
       return temp
 
+
 # For legacy compatibility - don't use this
 class rm2(rm):
   def __init__ (self):
-    device.__init__(self, None, None)
+    device.__init__(self, None, None, None)
 
   def discover(self):
     dev = discover()
@@ -557,6 +602,178 @@ class rm2(rm):
     self.mac = dev.mac
 
 
+class hysen(device):
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
+    self.type = "Hysen heating controller"
+
+  # Send a request
+  # input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00])
+  # Returns decrypted payload
+  # New behaviour: raises a ValueError if the device response indicates an error or CRC check fails
+  # The function prepends length (2 bytes) and appends CRC
+  def send_request(self,input_payload):
+
+    from PyCRC.CRC16 import CRC16
+    crc = CRC16(modbus_flag=True).calculate(bytes(input_payload))
+
+    # first byte is length, +2 for CRC16
+    request_payload = bytearray([len(input_payload) + 2,0x00])
+    request_payload.extend(input_payload)
+
+    # append CRC
+    request_payload.append(crc & 0xFF)
+    request_payload.append((crc >> 8) & 0xFF)
+
+    # send to device
+    response = self.send_packet(0x6a, request_payload)
+
+    # check for error
+    err = response[0x22] | (response[0x23] << 8)
+    if err:
+      raise ValueError('broadlink_response_error',err)
+
+    response_payload = bytearray(self.decrypt(bytes(response[0x38:])))
+
+    # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc)
+    response_payload_len = response_payload[0]
+    if response_payload_len + 2 > len(response_payload):
+      raise ValueError('hysen_response_error','first byte of response is not length')
+    crc = CRC16(modbus_flag=True).calculate(bytes(response_payload[2:response_payload_len]))
+    if (response_payload[response_payload_len] == crc & 0xFF) and (response_payload[response_payload_len+1] == (crc >> 8) & 0xFF):
+      return response_payload[2:response_payload_len]
+    else:
+      raise ValueError('hysen_response_error','CRC check on response failed')
+
+
+  # Get current room temperature in degrees celsius
+  def get_temp(self):
+    payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x08]))
+    return payload[0x05] / 2.0
+
+  # Get current external temperature in degrees celsius
+  def get_external_temp(self):
+    payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x08]))
+    return payload[18] / 2.0
+
+  # Get full status (including timer schedule)
+  def get_full_status(self):
+    payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x16]))
+    data = {}
+    data['remote_lock'] =  payload[3] & 1
+    data['power'] =  payload[4] & 1
+    data['active'] =  (payload[4] >> 4) & 1
+    data['temp_manual'] =  (payload[4] >> 6) & 1
+    data['room_temp'] =  (payload[5] & 255)/2.0
+    data['thermostat_temp'] =  (payload[6] & 255)/2.0
+    data['auto_mode'] =  payload[7] & 15
+    data['loop_mode'] =  (payload[7] >> 4) & 15
+    data['sensor'] = payload[8]
+    data['osv'] = payload[9]
+    data['dif'] = payload[10]
+    data['svh'] = payload[11]
+    data['svl'] = payload[12]
+    data['room_temp_adj'] = ((payload[13] << 8) + payload[14])/2.0
+    if data['room_temp_adj'] > 32767:
+      data['room_temp_adj'] = 32767 - data['room_temp_adj']
+    data['fre'] = payload[15]
+    data['poweron'] = payload[16]
+    data['unknown'] = payload[17]
+    data['external_temp'] = (payload[18] & 255)/2.0
+    data['hour'] =  payload[19]
+    data['min'] =  payload[20]
+    data['sec'] =  payload[21]
+    data['dayofweek'] =  payload[22]
+
+    weekday = []
+    for i in range(0, 6):
+      weekday.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0})
+
+    data['weekday'] = weekday
+    weekend = []
+    for i in range(6, 8):
+      weekend.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0})
+
+    data['weekend'] = weekend
+    return data
+
+  # Change controller mode
+  # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode.
+  # Manual mode will activate last used temperature.  In typical usage call set_temp to activate manual control and set temp.
+  # loop_mode refers to index in [ "12345,67", "123456,7", "1234567" ]
+  # E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday follow the "weekend" schedule
+  # loop_mode = 2 ("1234567") means every day (including Saturday and Sunday) follows the "weekday" schedule
+  # The sensor command is currently experimental
+  def set_mode(self, auto_mode, loop_mode,sensor=0):
+    mode_byte = ( (loop_mode + 1) << 4) + auto_mode
+    # print 'Mode byte: 0x'+ format(mode_byte, '02x')
+    self.send_request(bytearray([0x01,0x06,0x00,0x02,mode_byte,sensor]))
+
+  # Advanced settings
+  # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, 2 for internal control temperature, external limit temperature. Factory default: 0.
+  # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C
+  # Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C
+  # Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C
+  # Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C
+  # Actual temperature calibration (AdJ) adj = -0.5. Prescision 0.1C
+  # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down, 1 for anti-freezing function open. Factory default: 0
+  # Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0
+  def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron):
+    input_payload = bytearray([0x01,0x10,0x00,0x02,0x00,0x05,0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj*2)>>8 & 0xff), (int(adj*2) & 0xff), fre, poweron])
+    self.send_request(input_payload)
+
+  # For backwards compatibility only.  Prefer calling set_mode directly.  Note this function invokes loop_mode=0 and sensor=0.
+  def switch_to_auto(self):
+    self.set_mode(auto_mode=1, loop_mode=0)
+
+  def switch_to_manual(self):
+    self.set_mode(auto_mode=0, loop_mode=0)
+
+  # Set temperature for manual mode (also activates manual mode if currently in automatic)
+  def set_temp(self, temp):
+    self.send_request(bytearray([0x01,0x06,0x00,0x01,0x00,int(temp * 2)]) )
+
+  # Set device on(1) or off(0), does not deactivate Wifi connectivity.  Remote lock disables control by buttons on thermostat.
+  def set_power(self, power=1, remote_lock=0):
+    self.send_request(bytearray([0x01,0x06,0x00,0x00,remote_lock,power]) )
+
+  # set time on device
+  # n.b. day=1 is Monday, ..., day=7 is Sunday
+  def set_time(self, hour, minute, second, day):
+    self.send_request(bytearray([0x01,0x10,0x00,0x08,0x00,0x02,0x04, hour, minute, second, day ]))
+
+  # Set timer schedule
+  # Format is the same as you get from get_full_status.
+  # weekday is a list (ordered) of 6 dicts like:
+  # {'start_hour':17, 'start_minute':30, 'temp': 22 }
+  # Each one specifies the thermostat temp that will become effective at start_hour:start_minute
+  # weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon)
+  def set_schedule(self,weekday,weekend):
+    # Begin with some magic values ...
+    input_payload = bytearray([0x01,0x10,0x00,0x0a,0x00,0x0c,0x18])
+
+    # Now simply append times/temps
+    # weekday times
+    for i in range(0, 6):
+      input_payload.append( weekday[i]['start_hour'] )
+      input_payload.append( weekday[i]['start_minute'] )
+
+    # weekend times
+    for i in range(0, 2):
+      input_payload.append( weekend[i]['start_hour'] )
+      input_payload.append( weekend[i]['start_minute'] )
+
+    # weekday temperatures
+    for i in range(0, 6):
+      input_payload.append( int(weekday[i]['temp'] * 2) )
+
+    # weekend temperatures
+    for i in range(0, 2):
+      input_payload.append( int(weekend[i]['temp'] * 2) )
+
+    self.send_request(input_payload)
+
+
 S1C_SENSORS_TYPES = {
     0x31: 'Door Sensor',  # 49 as hex
     0x91: 'Key Fob',  # 145 as hex, as serial on fob corpse
@@ -613,6 +830,53 @@ class S1C(device):
         return result
 
 
+class dooya(device):
+  def __init__ (self, host, mac, devtype):
+    device.__init__(self, host, mac, devtype)
+    self.type = "Dooya DT360E"
+
+  def _send(self, magic1, magic2):
+    packet = bytearray(16)
+    packet[0] = 0x09
+    packet[2] = 0xbb
+    packet[3] = magic1
+    packet[4] = magic2
+    packet[9] = 0xfa
+    packet[10] = 0x44
+    response = self.send_packet(0x6a, packet)
+    err = response[0x22] | (response[0x23] << 8)
+    if err == 0:
+      payload = self.decrypt(bytes(response[0x38:]))
+      return ord(payload[4])
+
+  def open(self):
+    return self._send(0x01, 0x00)
+
+  def close(self):
+    return self._send(0x02, 0x00)
+
+  def stop(self):
+    return self._send(0x03, 0x00)
+
+  def get_percentage(self):
+    return self._send(0x06, 0x5d)
+
+  def set_percentage_and_wait(self, new_percentage):
+    current = self.get_percentage()
+    if current > new_percentage:
+      self.close()
+      while current is not None and current > new_percentage:
+        time.sleep(0.2)
+        current = self.get_percentage()
+
+    elif current < new_percentage:
+      self.open()
+      while current is not None and current < new_percentage:
+        time.sleep(0.2)
+        current = self.get_percentage()
+    self.stop()
+
+
 # Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode.
 # Only tested with Broadlink RM3 Mini (Blackbean)
 def setup(ssid, password, security_mode):
diff --git a/cli/README.md b/cli/README.md
index 47b45f4c97d3..5d7b3be19302 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -13,7 +13,7 @@ You should have the broadlink python installed, this can be made in many linux d
 sudo pip install broadlink
 ```
 
-Instalation
+Installation
 -----------
 Just copy this files
 
@@ -73,3 +73,8 @@ Get Temperature :
 ```
 broadlink_cli --device @BEDROOM.device --temperature
 ```
+
+Get Energy Consumption (For a SmartPlug) :
+```
+broadlink_cli --device @BEDROOM.device --energy
+```
diff --git a/cli/broadlink_cli b/cli/broadlink_cli
index 1ff45f1d9b3c..989c6c118ba7 100755
--- a/cli/broadlink_cli
+++ b/cli/broadlink_cli
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 
 import broadlink
+import sys
 import argparse
 import time
 
@@ -67,11 +68,19 @@ parser.add_argument("--type", type=auto_int, default=0x2712, help="type of devic
 parser.add_argument("--host", help="host address")
 parser.add_argument("--mac", help="mac address (hex reverse), as used by python-broadlink library")
 parser.add_argument("--temperature",action="store_true", help="request temperature from device")
+parser.add_argument("--energy",action="store_true", help="request energy consumption from device")
+parser.add_argument("--check", action="store_true", help="check current power state")
+parser.add_argument("--checknl", action="store_true", help="check current nightlight state")
+parser.add_argument("--turnon", action="store_true", help="turn on device")
+parser.add_argument("--turnoff", action="store_true", help="turn off device")
+parser.add_argument("--turnnlon", action="store_true", help="turn on nightlight on the device")
+parser.add_argument("--turnnloff", action="store_true", help="turn off nightlight on the device")
+parser.add_argument("--switch", action="store_true", help="switch state from on to off and off to on")
 parser.add_argument("--send", action="store_true", help="send command")
 parser.add_argument("--sensors", action="store_true", help="check all sensors")
 parser.add_argument("--learn", action="store_true", help="learn command")
-parser.add_argument("--learnfile", help="save learned command to a specified file")
 parser.add_argument("--rfscanlearn", action="store_true", help="rf scan learning")
+parser.add_argument("--learnfile", help="save learned command to a specified file")
 parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format")
 parser.add_argument("--convert", action="store_true", help="convert input data to durations")
 parser.add_argument("data", nargs='*', help="Data to send or convert")
@@ -79,7 +88,7 @@ args = parser.parse_args()
 
 if args.device:
     values = args.device.split()
-    type = int(values[0], 0)
+    type = int(values[0],0)
     host = values[1]
     mac = bytearray.fromhex(values[2])
 elif args.mac:
@@ -87,7 +96,7 @@ elif args.mac:
     host = args.host
     mac = bytearray.fromhex(args.mac)
 
-if args.host or host is not None:
+if args.host or args.device:
     dev = broadlink.gendevice(type, (host, 80), mac)
     dev.auth()
 
@@ -97,6 +106,8 @@ if args.convert:
     print(format_durations(durations))
 if args.temperature:
     print(dev.check_temperature())
+if args.energy:
+    print(dev.get_energy())
 if args.sensors:
     try:
         data = dev.check_sensors()
@@ -122,14 +133,55 @@ if args.learn:
         learned = format_durations(to_microseconds(bytearray(data))) \
             if args.durations \
             else ''.join(format(x, '02x') for x in bytearray(data))
-        if args.learnfile is None:
+        if args.learn:
             print(learned)
-        if args.learnfile is not None:
+        if args.learnfile:
             print("Saving to {}".format(args.learnfile))
             with open(args.learnfile, "w") as text_file:
                 text_file.write(learned)
     else:
         print("No data received...")
+if args.check:
+    if dev.check_power():
+        print('* ON *')
+    else:
+        print('* OFF *')
+if args.checknl:
+    if dev.check_nightlight():
+        print('* ON *')
+    else:
+        print('* OFF *')
+if args.turnon:
+    dev.set_power(True)
+    if dev.check_power():
+        print('== Turned * ON * ==')
+    else:
+        print('!! Still OFF !!')
+if args.turnoff:
+    dev.set_power(False)
+    if dev.check_power():
+        print('!! Still ON !!')
+    else:
+        print('== Turned * OFF * ==')
+if args.turnnlon:
+    dev.set_nightlight(True)
+    if dev.check_nightlight():
+        print('== Turned * ON * ==')
+    else:
+        print('!! Still OFF !!')
+if args.turnnloff:
+    dev.set_nightlight(False)
+    if dev.check_nightlight():
+        print('!! Still ON !!')
+    else:
+        print('== Turned * OFF * ==')
+if args.switch:
+    if dev.check_power():
+        dev.set_power(False)
+        print('* Switch to OFF *')
+    else:
+        dev.set_power(True)
+        print('* Switch to ON *')
 if args.rfscanlearn:
     dev.sweep_frequency()
     print("Learning RF Frequency, press and hold the button to learn...")
@@ -174,4 +226,4 @@ if args.rfscanlearn:
             with open(args.learnfile, "w") as text_file:
                 text_file.write(learned)
     else:
-        print("No data received...")
\ No newline at end of file
+        print("No data received...")
diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery
index 4a7438dfb7a4..385f1932467f 100755
--- a/cli/broadlink_discovery
+++ b/cli/broadlink_discovery
@@ -8,17 +8,15 @@ parser = argparse.ArgumentParser(fromfile_prefix_chars='@');
 parser.add_argument("--timeout", type=int, default=5, help="timeout to wait for receiving discovery responses")
 args = parser.parse_args()
 
-print "discover"
+print "Discovering..."
 devices = broadlink.discover(timeout=args.timeout)
-#print devices
 for device in devices:
     if device.auth():
         print "###########################################"
-#	    print device
         print device.type
-        print "# broadlink_cli --type 0x2712 --host {} --mac {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac))
+        print "# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))
         print "Device file data (to be used with --device @filename in broadlink_cli) : "
-        print "0x2712 {} {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac))
+        print "{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))
         if hasattr(device, 'check_temperature'):
             print "temperature = {}".format(device.check_temperature())
         print ""
diff --git a/requirements.txt b/requirements.txt
index f7d3e237ac67..9b20c33eb44b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1 @@
-pycrypto==2.6.1
+pycryptodome==3.6.6
diff --git a/setup.py b/setup.py
index f8d1f4cab807..f4482eff56d1 100644
--- a/setup.py
+++ b/setup.py
@@ -10,13 +10,16 @@ try:
     import pyaes
     dynamic_requires = ["pyaes==1.6.0"]
 except ImportError as e:
-    dynamic_requires = ['pycrypto==2.6.1']
+    dynamic_requires = ['pycryptodome==3.6.6']
 
-version = 0.6
+# For Hysen thermostatic heating controller
+dynamic_requires.append('PyCRC')
+
+version = 0.9
 
 setup(
     name='broadlink',
-    version=0.6,
+    version=0.9,
     author='Matthew Garrett',
     author_email='mjg59@srcf.ucam.org',
     url='http://github.com/mjg59/python-broadlink',