Skip to content
Open
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
include LICENSE.txt
include README.rst
include README.md
include docs/*
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ LTC: Lajqnm28UipLbzJqvyy4tRQFf39xQy6B48

## Install ##

It's super easy - just use pip to get the latest version.
It's super easy - just use pip to get the latest version (Python 3 is **required**):

```
pip install nanoleaf --upgrade
```

If you get an error similar to `Command '['git', 'tag', '-l', '--points-at', 'HEAD']' returned non-zero exit status 128`, this is because you attempted an installation using pip from Python 2.

## Setup ##

There are two pieces of information you'll need to control your Aurora: The IP address and an auth token.
Expand Down
121 changes: 105 additions & 16 deletions nanoleaf/aurora.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,38 @@
import random
import colorsys
import re
import socket

# Primary interface for an Aurora light
# For instructions or bug reports, please visit
# https://github.com/software-2/nanoleaf


class AuroraStream(object):
def __init__(self, addr: str, port: int):
self._prepare = []
self.addr = (addr, port)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.settimeout(1)

def __send(self, msg: bytes):
self.sock.sendto(msg, self.addr)

def panel_set(self, panel_id: int, red: int, green: int, blue: int,
white: int = 0, transition_time: int = 1):
b = bytes([1, panel_id, 1, red, green, blue, white, transition_time])
self.__send(b)

def panel_prepare(self, panel_id: int, red: int, green: int, blue: int,
white: int = 0, transition_time: int = 1):
self._prepare = self._prepare + [panel_id, 1, red, green, blue, white, transition_time]

def panel_strobe(self):
data = [len(self._prepare)] + self._prepare
self._prepare = []
self.__send(bytes(data))


class Aurora(object):
def __init__(self, ip_address: str, auth_token: str):
self.baseUrl = "http://" + ip_address + ":16021/api/v1/" + auth_token + "/"
Expand Down Expand Up @@ -46,7 +72,7 @@ def __delete(self, endpoint: str = "") -> requests.request:

def __check_for_errors(self, r: requests.request) -> requests.request:
if r.status_code == 200:
if r.text == "": # BUG: Delete User returns 200, not 204 like it should, as of firmware 1.5.0
if r.text == "": # BUG: Identify returns 200, not 204 like it should, as of firmware 2.2.0
return None
return r.json()
elif r.status_code == 204:
Expand All @@ -72,8 +98,8 @@ def __check_for_errors(self, r: requests.request) -> requests.request:

@property
def info(self):
"""Returns the full Aurora Info request.
"""Returns the full Aurora Info request.

Useful for debugging since it's just a fat dump."""
return self.__get()

Expand All @@ -89,17 +115,17 @@ def identify(self):
@property
def firmware(self):
"""Returns the firmware version of the device"""
return self.__get("firmwareVersion")
return self.__get()['firmwareVersion']

@property
def model(self):
"""Returns the model number of the device. (Always returns 'NL22')"""
return self.__get("model")
return self.__get()['model']

@property
def serial_number(self):
"""Returns the serial number of the device"""
return self.__get("serialNo")
return self.__get()['serialNo']

def delete_user(self):
"""CAUTION: Revokes your auth token from the device."""
Expand Down Expand Up @@ -254,16 +280,12 @@ def color_temperature(self, level):
@property
def color_temperature_min(self):
"""Returns the minimum color temperature possible. (This always returns 1200)"""
# return self.__get("state/ct/min")
# BUG: Firmware 1.5.0 returns the wrong value.
return 1200
return self.__get("state/ct/min")

@property
def color_temperature_max(self):
"""Returns the maximum color temperature possible. (This always returns 6500)"""
# return self.__get("state/ct/max")
# BUG: Firmware 1.5.0 returns the wrong value.
return 6500
return self.__get("state/ct/max")

def color_temperature_raise(self, level):
"""Raise the color temperature of the device by a relative amount (negative lowers color temperature)"""
Expand Down Expand Up @@ -349,7 +371,11 @@ def orientation_max(self):
@property
def panel_count(self):
"""Returns the number of panels connected to the device"""
return self.__get("panelLayout/layout/numPanels")
# Firmware 2.2.0 has a bug where the rhythm module is added to the panel count.
count = int(self.__get("panelLayout/layout/numPanels"))
if self.rhythm_connected:
count -= 1
return count

@property
def panel_length(self):
Expand All @@ -359,7 +385,7 @@ def panel_length(self):
@property
def panel_positions(self):
"""Returns a list of all panels with their attributes represented in a dict.

panelId - Unique identifier for this panel
x - X-coordinate
y - Y-coordinate
Expand Down Expand Up @@ -391,7 +417,7 @@ def effects_list(self):

def effect_random(self) -> str:
"""Sets the active effect to a new random effect stored on the device.

Returns the name of the new effect."""
effect_list = self.effects_list
active_effect = self.effect
Expand All @@ -406,7 +432,7 @@ def effect_set_raw(self, effect_data: dict):

The dict given must match the json structure specified in the API docs."""
data = {"write": effect_data}
self.__put("effects", data)
return self.__put("effects", data)

def effect_details(self, name: str) -> dict:
"""Returns the dict containing details for the effect specified"""
Expand All @@ -431,3 +457,66 @@ def effect_rename(self, old_name: str, new_name: str):
"animName": old_name,
"newName": new_name}}
self.__put("effects", data)

def effect_stream(self):
"""Open an external control stream"""
data = {"write": {"command": "display",
"animType": "extControl"}}

udp_info = self.__put("effects", data)
return AuroraStream(udp_info["streamControlIpAddr"], udp_info["streamControlPort"])

###########################################
# Rhythm methods
###########################################

@property
def rhythm_connected(self):
"""Returns True if the rhythm module is connected, False if it's not"""
return self.__get("rhythm/rhythmConnected")

@property
def rhythm_active(self):
"""Returns True if the rhythm microphone is active, False if it's not"""
return self.__get("rhythm/rhythmActive")

@property
def rhythm_id(self):
"""Returns the ID of the rhythm module"""
return self.__get("rhythm/rhythmId")

@property
def rhythm_hardware_version(self):
"""Returns the hardware version of the rhythm module"""
return self.__get("rhythm/hardwareVersion")

@property
def rhythm_firmware_version(self):
"""Returns the firmware version of the rhythm module"""
return self.__get("rhythm/firmwareVersion")

@property
def rhythm_aux_available(self):
"""Returns True if an aux cable is connected to the rhythm module, False if it's not"""
return self.__get("rhythm/auxAvailable")

@property
def rhythm_mode(self):
"""Returns the sound source of the rhythm module. 0 for microphone, 1 for aux cable"""
return self.__get("rhythm/rhythmMode")

@rhythm_mode.setter
def rhythm_mode(self, value):
"""Set the sound source of the rhythm module. 0 for microphone, 1 for aux cable"""
data = {"rhythmMode": value}
self.__put("rhythm", data)

@property
def rhythm_position(self):
"""Returns the position and orientation of the rhythm module represented in a dict.

x - X-coordinate
y - Y-coordinate
o - Rotational orientation
"""
return self.__get("rhythm/rhythmPos")
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[metadata]
description-file = README.rst
description-file = README.md
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
packages=['nanoleaf'],
version=gitVersion,
description='Python interface for Nanoleaf Aurora.',
long_description=open('README.rst', 'r').read(),
long_description=open('README.md', 'r').read(),
author='Anthony Bryan',
author_email='[email protected]',
url='https://github.com/software-2/nanoleaf',
Expand Down