├── LICENSE ├── README.md └── magichome.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Kemepnich 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 | # MagicHome-Python API 2 | MagicHome Wifi protocol for Python. 3 | 4 | This utility was designed for use with devices compatible with the MagicHome Wifi app. 5 | 6 | ### This project is inactive and I am not working on it. I will gladly review and merge pull requests, though! 7 | 8 | It currently supports: 9 | * Bulbs (Firmware v.4 and greater) 10 | * Legacy Bulbs (Firmware v.3 and lower) 11 | * RGB Controllers 12 | * RGB+WW Controllers 13 | * RGB+WW+CW Controllers 14 | 15 | ##### Commands: 16 | * Turning devices on and off 17 | * Getting current device states 18 | * Setting either colors, or whites (in bulbs) 19 | * Setting colors (+ WW + CW) in compatible devices 20 | * Sending preset functions 21 | 22 | ##### Tasks: 23 | * Initial device setup via UDP. 24 | * Custom commands (I know how it works ... just not a priority to implement currently.) 25 | * Setting the device's internal clock. 26 | 27 | ##### Notes: 28 | Device types are: 29 | * 0: RGB 30 | * 1: RGB+WW 31 | * 2: RGB+WW+CW 32 | * 3: Bulb (v.4+) 33 | * 4: Bulb (v.3-) 34 | (Higher numbers reserved for future use) 35 | 36 | Functions accept integers 0-255 as parameters. 37 | 38 | To create a new instance of the API, use the following terminology: 39 | 40 | ```python 41 | controller1 = MagicHomeApi('IP Address', [Device Type (from above)]) 42 | ``` 43 | 44 | And then call its functions in the following manner: 45 | 46 | Get the status of a device: 47 | 48 | ```python 49 | controller1.get_status() 50 | ``` 51 | 52 | To turn a device on: 53 | 54 | ```python 55 | controller1.turn_on 56 | ``` 57 | 58 | And similarly, to turn it off: 59 | 60 | ```python 61 | controller1.turn_off 62 | ``` 63 | 64 | To update a colored device, send R, G, and B to it. 65 | 66 | ```python 67 | controller1.update_device(R, G, B) 68 | ``` 69 | 70 | And if that device supports WW/CW (or both): 71 | 72 | ```python 73 | controller1.update_device(R, G, B, WW, CW) 74 | ``` 75 | 76 | BUT, if you want to update a bulb's white level, send R,G,B AND W... only W's level will be used. 77 | 78 | ```python 79 | controller1.update_device(R, G, B, W) 80 | ``` 81 | 82 | To Update a Bulb's color, you don't need to send the W parameter. 83 | Finally, to send a preset command: 84 | 85 | ```python 86 | controller1.send_preset_function(25, 100) 87 | ``` 88 | 89 | Presets can range from 0x25 (int 37) to 0x38 (int 56), anything outside of this will be rounded up or down. 90 | 91 | A speed of 10 0% is fastest, and 0 % is super duper slow. 92 | 93 | Copyright 2016 Adam Kempenich. Licensed under MIT. 94 | 95 | Questions? Comments? Feedback of any kind? Find me on Github, @AdamKempenich 96 | -------------------------------------------------------------------------------- /magichome.py: -------------------------------------------------------------------------------- 1 | """MagicHome Python API. 2 | 3 | Copyright 2016, Adam Kempenich. Licensed under MIT. 4 | 5 | It currently supports: 6 | - Bulbs (Firmware v.4 and greater) 7 | - Legacy Bulbs (Firmware v.3 and lower) 8 | - RGB Controllers 9 | - RGB+WW Controllers 10 | - RGB+WW+CW Controllers 11 | """ 12 | import socket 13 | import csv 14 | import struct 15 | import datetime 16 | 17 | 18 | class MagicHomeApi: 19 | """Representation of a MagicHome device.""" 20 | 21 | def __init__(self, device_ip, device_type, keep_alive=True): 22 | """"Initialize a device.""" 23 | self.device_ip = device_ip 24 | self.device_type = device_type 25 | self.API_PORT = 5577 26 | self.latest_connection = datetime.datetime.now() 27 | self.keep_alive = keep_alive 28 | self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 | self.s.settimeout(3) 30 | try: 31 | print("Establishing connection with the device.") 32 | self.s.connect((self.device_ip, self.API_PORT)) 33 | except socket.error as exc: 34 | print("Caught exception socket.error : %s" % exc) 35 | if self.s: 36 | self.s.close() 37 | 38 | def turn_on(self): 39 | """Turn a device on.""" 40 | self.send_bytes(0x71, 0x23, 0x0F, 0xA3) if self.device_type < 4 else self.send_bytes(0xCC, 0x23, 0x33) 41 | 42 | def turn_off(self): 43 | """Turn a device off.""" 44 | self.send_bytes(0x71, 0x24, 0x0F, 0xA4) if self.device_type < 4 else self.send_bytes(0xCC, 0x24, 0x33) 45 | 46 | def get_status(self): 47 | """Get the current status of a device.""" 48 | if self.device_type == 2: 49 | self.send_bytes(0x81, 0x8A, 0x8B, 0x96) 50 | return self.s.recv(15) 51 | else: 52 | self.send_bytes(0x81, 0x8A, 0x8B, 0x96) 53 | return self.s.recv(14) 54 | 55 | def update_device(self, r=0, g=0, b=0, white1=None, white2=None): 56 | """Updates a device based upon what we're sending to it. 57 | 58 | Values are excepted as integers between 0-255. 59 | Whites can have a value of None. 60 | """ 61 | if self.device_type <= 1: 62 | # Update an RGB or an RGB + WW device 63 | white1 = self.check_number_range(white1) 64 | message = [0x31, r, g, b, white1, 0x00, 0x0f] 65 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 66 | 67 | elif self.device_type == 2: 68 | # Update an RGB + WW + CW device 69 | message = [0x31, 70 | self.check_number_range(r), 71 | self.check_number_range(g), 72 | self.check_number_range(b), 73 | self.check_number_range(white1), 74 | self.check_number_range(white2), 75 | 0x0f, 0x0f] 76 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 77 | 78 | elif self.device_type == 3: 79 | # Update the white, or color, of a bulb 80 | if white1 is not None: 81 | message = [0x31, 0x00, 0x00, 0x00, 82 | self.check_number_range(white1), 83 | 0x0f, 0x0f] 84 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 85 | else: 86 | message = [0x31, 87 | self.check_number_range(r), 88 | self.check_number_range(g), 89 | self.check_number_range(b), 90 | 0x00, 0xf0, 0x0f] 91 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 92 | 93 | elif self.device_type == 4: 94 | # Update the white, or color, of a legacy bulb 95 | if white1 != None: 96 | message = [0x56, 0x00, 0x00, 0x00, 97 | self.check_number_range(white1), 98 | 0x0f, 0xaa, 0x56, 0x00, 0x00, 0x00, 99 | self.check_number_range(white1), 100 | 0x0f, 0xaa] 101 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 102 | else: 103 | message = [0x56, 104 | self.check_number_range(r), 105 | self.check_number_range(g), 106 | self.check_number_range(b), 107 | 0x00, 0xf0, 0xaa] 108 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 109 | else: 110 | # Incompatible device received 111 | print("Incompatible device type received...") 112 | 113 | def check_number_range(self, number): 114 | """Check if the given number is in the allowed range.""" 115 | if number < 0: 116 | return 0 117 | elif number > 255: 118 | return 255 119 | else: 120 | return number 121 | 122 | def send_preset_function(self, preset_number, speed): 123 | """Send a preset command to a device.""" 124 | # Presets can range from 0x25 (int 37) to 0x38 (int 56) 125 | if preset_number < 37: 126 | preset_number = 37 127 | if preset_number > 56: 128 | preset_number = 56 129 | if speed < 0: 130 | speed = 0 131 | if speed > 100: 132 | speed = 100 133 | 134 | if type == 4: 135 | self.send_bytes(0xBB, preset_number, speed, 0x44) 136 | else: 137 | message = [0x61, preset_number, speed, 0x0F] 138 | self.send_bytes(*(message+[self.calculate_checksum(message)])) 139 | 140 | def calculate_checksum(self, bytes): 141 | """Calculate the checksum from an array of bytes.""" 142 | return sum(bytes) & 0xFF 143 | 144 | def send_bytes(self, *bytes): 145 | """Send commands to the device. 146 | 147 | If the device hasn't been communicated to in 5 minutes, reestablish the 148 | connection. 149 | """ 150 | check_connection_time = (datetime.datetime.now() - 151 | self.latest_connection).total_seconds() 152 | try: 153 | if check_connection_time >= 290: 154 | print("Connection timed out, reestablishing.") 155 | self.s.connect((self.device_ip, self.API_PORT)) 156 | message_length = len(bytes) 157 | self.s.send(struct.pack("B"*message_length, *bytes)) 158 | # Close the connection unless requested not to 159 | if self.keep_alive is False: 160 | self.s.close 161 | except socket.error as exc: 162 | print("Caught exception socket.error : %s" % exc) 163 | if self.s: 164 | self.s.close() 165 | --------------------------------------------------------------------------------