Upto: Table of Contents of full book "Internet of Things - a techie's viewpoint"

WiFi lights: Yeelight

There are an increasing number of WiFi enabled lights. Some are purely WiFi, others use some protocol such as Zigbee to talk to a hub which in turn has a WiFi interface. Included in the second group are the Phillips Hue, the Cree lights, the GE lights, while in the first group are LIFX. IKEA lights and Yeelight. This chapter looks at the Yeelight bulbs, as both typical examples and with its own unique features.

Resources

Yeelight bulbs

Yeelight make a number of lights, including a ceiling light, desk light, and screw-in GU10 white and colour bulbs. In Australia these run on 240 volts.

Apps and services

Yeelight have apps for IoS and Android. They also claim to integrate with IFTT, Amazon Alexa and Google Assistant. Linking a Yeelight to Google Assistant is described at Yeelight work with Google Home . It took me a while to get mine working, as I somehow managed to create two accounts on xiaomi.com, one for my email and a separate one for my phone. Only one can be linked to the Yeelight and I kept trying the other, until I discovered my error (thanks to yusure of the Yeelight staff - very patient and helpful!)

The Google app works fine, and presumably the Amazon app works fine too.

Google Home control

The voice commands supported by Google Home are

The following commands are supported:

Turn on/off:
1. Turn on/off [target_device]

Dim:
1. Dim [target_device]
2. Dim [target_device] to xx%
3. Set brightness of [target_device] to xx%
4. Turn brightness of [target_device] to maximum/minimum

Color:
1. Set [target_device] to [color]
2. Change [target_device] to [color]

Scene:
1. Activate [target_scene]

Notice:
1. target_device could be described as “device name”, “all lights”, “all lights in some room”
2. target_scene is the scene you setup in Yeelight app
	

LAN control

Most of the home IoT devices require internet access to a cloud server, so that whatever you do with the device is potentially recorded at a third party site. This method of access is probably required if you need to control the device from a remote location, but if you are on the same network, there shouldn't be any need for this.

Yeelight recognise this and offer a mechanism whereby you can control a Yeelight directly on your LAN. This is described in the Yeelight WiFi Light Inter-Operation Specification .

What that document doesn't describe is how to set up your Yeelight to work on the LAN. You have to enable the "Control LAN" mode, which is found by selecting a yeelight device, scrolling down to the bottom of the screen and choosing the rightmost icon and then the Control LAN icon. The following images from Yeelight staff show this:

Note taht if you do a hard reset on your bulb, you will need to additionally reset Lan Control.

Adverts and search

The Yeelight when enabled for LAN mode broadcasts about every 3 minutes a multicast packet to multicast group 239.255.255.250 on port 1982. This looks like


NOTIFY * HTTP/1.1
Host: 239.255.255.250:1982
Cache-Control: max-age=3584
Location: yeelight://192.168.1.25:55443
NTS: ssdp:alive
Server: POSIX, UPnP/1.0 YGLC/1
id: 0x00000000035ed2f1
model: color
fw_ver: 57
support: get_prop set_default set_power toggle set_bright start_cf stop_cf set_scene cron_add cron_get cron_del set_ct_abx set_rgb set_hsv set_adjust adjust_bright adjust_ct adjust_color set_music set
power: on
bright: 50
color_mode: 1
ct: 4000
rgb: 16261119
hue: 359
sat: 100
      
The fourth line is the important one for control and information. It says that the Yeelight will be running a TCP server at address 192.168.1.25 on port 55443.

A python program to listen and print this information is listen.py:


import socket
import struct
import datetime

MCAST_GRP = '239.255.255.250'
MCAST_PORT = 1982

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', MCAST_PORT))  # use MCAST_GRP instead of '' to listen only
                             # to MCAST_GRP, not all groups on MCAST_PORT
mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)

sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

print ('Listening')
while True:
  print sock.recv(10240)
  print datetime.datetime.now()

      

In addition, a muticast query can be made of all Yeelight bulbs on the network, using the same multicast address and port. A bulb will reply on the same port as the request came from. This is typically a random port, so it is easiest to fix the port. A Python program to do this is send.py:


import socket
import struct

MCAST_GRP = '239.255.255.250'
MCAST_PORT = 1982
SRC_PORT = 5159 # my random port

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
sock.bind(('', SRC_PORT))
sock.sendto("M-SEARCH * HTTP/1.1\r\n\
HOST: 239.255.255.250:1982\r\n\
MAN: \"ssdp:discover\"\r\n\
ST: wifi_bulb\r\n", (MCAST_GRP, MCAST_PORT))

# close this multicast socket
sock.close()

# and open a new receive socket on the same port
sock_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock_recv.bind(('', SRC_PORT))

while True:
  print sock_recv.recv(10240)

      

Commands

Commands are sent on the TCP port as a JSON string. The set of commands are given in the Yeelight WiFi Light Inter-Operation Specification . Results are returned as a JSON string, which can be parsed in Python using the json package.

The following program uses multicast to get the TCP address and port and then executes a series of commands on the first light that answers. It defines procedures to get_prop, set_power and set_bright. Each command opens a new TCP socket, sends the command, parses the response, closes the socket and returns the result.

The program is command.py:


import socket
import struct
import re
import json
import time

MCAST_GRP = '239.255.255.250'
MCAST_PORT = 1982
SRC_PORT = 5159 # my random port

CR_LF = "\r\n"

def get_ip_port():
  return ('192.168.1.25', 55443)

  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
  sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
  sock.bind(('', SRC_PORT))
  # need to ensure byte format for Python3
  command = b"M-SEARCH * HTTP/1.1\r\n\
HOST: 239.255.255.250:1982\r\n\
MAN: \"ssdp:discover\"\r\n\
ST: wifi_bulb\r\n"
  #sock.sendto(bytes(command, 'utf-8'), (MCAST_GRP, MCAST_PORT))
  sock.sendto(command, (MCAST_GRP, MCAST_PORT))
  sock.close()
  
  sock_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
                            socket.IPPROTO_UDP)
  # ensure this socket is listening on the same
  # port as the multicast went on
  sock_recv.bind(('', SRC_PORT))
  response = sock_recv.recv(10240)
  response = response.decode('utf-8')
  sock_recv.close()

  # match on a line like "Location: yeelight://192.168.1.25:55443"
  # to pull ip out of group(1), port out of group(2)
  prog = re.compile("Location: yeelight://(\d*\.\d*\.\d*\.\d*):(\d*).*")
  for line in response.splitlines():
    result = prog.match(line)
    if result != None:
      ip = result.group(1)
      port = result.group(2)
      return (ip, int(port))
  return (None, None)

def sendto(ip, port, command):
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
  sock.connect((ip, port))
  sock.send(bytes(command + CR_LF, 'utf-8'))
  response = sock.recv(10240)
  #print(response)

  # the response is a JSON string, parse it and return
  # the "result" field
  dict = json.loads(response)
  sock.close()
  #print "Response was ", response
  return(dict["result"])

def get_prop(prop, ip, port):
  # hard code the JSON string
  command = '{"id":1,"method":"get_prop","params":["' + prop + '"]}'
  response = sendto(ip, port, command)
  return response

def set_prop(prop, params, ip, port):
  # hard code the JSON string
  command = '{"id":1,"method":"set_' + prop +\
            '", "params":' + params +\
            '}'
  print(command)
  response = sendto(ip, port, command)
  return response

def set_name(name, ip, port):
  params = '["' + name + '"]'
  response = set_prop('name', params, ip, port)
  return response

def set_power(state, ip, port):
  params = '["' + state + '", "smooth", 500]'
  response = set_prop('power', params, ip, port)
  return response
  
def set_bright(state, ip, port):
  params = '[' + str(state) + ', "smooth", 500]'
  response = set_prop('bright', params, ip, port)
  return response
  

if __name__ == "__main__":
  print('Starting')
  (ip, port) = get_ip_port()
  if (ip, port) == (None, None):
    print("Can't get address of light")
    exit(1)
  print('IP is ', ip, ' port is ', port)

  time.sleep(5)
  # sample set commands:
  success = set_power("on", ip, port)
  print('Power set is', success[0])

  time.sleep(5)
  # sample set commands:
  success = set_power("off", ip, port)
  print('Power set is', success[0])
  #exit()
  
  time.sleep(5)
  # sample set commands:
  success = set_power("on", ip, port)
  print('Power set is', success[0])

  success = set_bright(90, ip, port)
  print('Brightness set is', success[0])
  
  name = set_name('bedroom', ip, port)
  print('Name is ', name[0])
  
  # sample get commands:
  power = get_prop("power", ip, port)
  print('Power is', power[0])
  
  bright = get_prop("bright", ip, port)
  print("Bright is ", bright[0])

  name = get_prop('name', ip, port)
  print("name now is", name[0])

  # getting multiple properties at once.
  # Be careful with the quotes, words need to be separated by "," !
  prop_list = get_prop('name", "power", "bright', ip, port)
  print("Property list is", prop_list)

  

      


Copyright © Jan Newmarch, jan@newmarch.name
Creative Commons License
"The Internet of Things - a techie's viewpoint" by Jan Newmarch is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Based on a work at https://jan.newmarch.name/IoT/.

If you like this book, please donate using PayPal