diff --git a/.gitignore b/.gitignore index 53268b7..57465e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ ignorecase = true +__pycache__ diff --git a/lights/__pycache__/discover.cpython-35.pyc b/lights/__pycache__/discover.cpython-35.pyc deleted file mode 100644 index b494d34..0000000 Binary files a/lights/__pycache__/discover.cpython-35.pyc and /dev/null differ diff --git a/lights/__pycache__/protocol.cpython-35.pyc b/lights/__pycache__/protocol.cpython-35.pyc deleted file mode 100644 index 46f7008..0000000 Binary files a/lights/__pycache__/protocol.cpython-35.pyc and /dev/null differ diff --git a/lights/__pycache__/smartbulb.cpython-35.pyc b/lights/__pycache__/smartbulb.cpython-35.pyc deleted file mode 100644 index f1d5c34..0000000 Binary files a/lights/__pycache__/smartbulb.cpython-35.pyc and /dev/null differ diff --git a/postgres/create_schema.sql b/postgres/create_schema.sql index 3026ac1..6b53d76 100644 --- a/postgres/create_schema.sql +++ b/postgres/create_schema.sql @@ -21,7 +21,7 @@ CREATE TABLE guests ( city text, state text, zip text, - room_uuid uuid REFERENCES rooms(uuid). + room_uuid uuid REFERENCES rooms(uuid) ); -- Sessions table to keep track of tokens for authentication. @@ -129,3 +129,14 @@ CREATE TABLE employees ( last_name text, admin boolean DEFAULT false ); + +-- Routing Table. +-- We can redirect based on peripheral OR by room. +CREATE TABLE routing ( + uuid uuid PRIMARY KEY DEFAULT gen_random_uuid(), + room_uuid uuid REFERENCES rooms(uuid), + peripheral_uuid uuid REFERENCES peripherals(uuid), + metric integer DEFAULT 0, + target text NOT NULL, + enabled BOOLEAN default true +); \ No newline at end of file diff --git a/web/Dockerfile b/web/Dockerfile index 02294a3..97e2a40 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -17,6 +17,8 @@ RUN pip install -r /src/requirements.txt COPY index.py /src/index.py COPY static/ManagementApp.html /src/static/ManagementApp.html COPY postgresLibrary.py /src/postgresLibrary.py +COPY peripherals.py /src/peripherals.py +COPY lights/ /src/lights/ # Add entrypoint COPY docker-entrypoint.sh /usr/local/bin/ diff --git a/web/index.py b/web/index.py index 9c3427c..82684b7 100644 --- a/web/index.py +++ b/web/index.py @@ -1,12 +1,14 @@ #!/usr/bin/python from flask import Flask from flask_restful import Resource, Api -from flask import request +from flask import request, Response from flask.json import jsonify from flask import render_template +import requests import json import postgresLibrary as pL import uuid +import peripherals as peripherals from datetime import date, datetime @@ -21,16 +23,10 @@ PORT_NUMBER = 8000 app = Flask(__name__) api = Api(app) -class Index(Resource): - def get(self): - # rooms = pL.getRooms() - # for room in rooms: - # print(room) - # room['pheripherals'] = pL.getPeripheralsbyRoom(room['uuid']) - # return rooms - return app.send_static_file('static/ManagementApp.html') - -api.add_resource(Index, '/') +# class Index(Resource): +# def get(self): +# return app.send_static_file('/src/static/ManagementApp.html') +# api.add_resource(Index, '/') #################################### @@ -41,6 +37,63 @@ api.add_resource(Index, '/') # Room Info #################################### +@app.route('/api/peripheral/', methods=['GET']) +def getPeripheral(uuid): + per = pL.getPeripheralbyUUID(uuid) + + return jsonify(per) + +@app.route('/api/peripheral/', methods=['POST']) +def setPeripheralID(uuid): + body = request.get_json() + + if body == None: + return None + + per = pL.getPeripheralbyUUID(uuid) + roomRoutes = pL.roomRoutingEntries(per['room_uuid']) + peripheralRoutes = pL.peripheralRoutingEntries(uuid) + + # At the room level, we simply proxy requests. + # If there's a specific peripheral IP in a peripheral route, it will be ignored if there is a room route. + if len(roomRoutes) == 0: + # We're doing a peripheral specific handling. + return peripherals.handlePeripheral(per, body, peripheralRoutes) + else: + dest = roomRoutes[0] + resp = requests.request( + method=request.method, + url='http://' + dest['target'] + request.script_root + request.full_path, + headers={key: value for (key, value) in request.headers if key != 'Host'}, + data=request.get_data(), + cookies=request.cookies, + timeout=0.5, + allow_redirects=False + ) + + excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] + headers = [(name, value) for (name, value) in resp.raw.headers.items() + if name.lower() not in excluded_headers] + + response = Response(resp.content, resp.status_code, headers) + return response + + +@app.route('/api/guest/rooms', methods=['GET','POST']) +def getGuestRooms(): + rooms = pL.getRooms() + for room in rooms: + print(room) + room['peripherals'] = pL.getPeripheralsbyRoom(room['uuid']) + return jsonify({ + 'rooms': rooms, + 'hotel_name': 'Hotel' + }) + +@app.route('/api/routes//entries', methods=['GET','POST']) +def getRoutes(uuid): + return jsonify(pL.routingEntries(uuid, uuid)) + # Get all rooms @app.route('/api/admin/rooms', methods=['GET', 'POST']) def getRooms(): diff --git a/lights/README.md b/web/lights/README.md similarity index 100% rename from lights/README.md rename to web/lights/README.md diff --git a/web/lights/__init__.py b/web/lights/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lights/discover.py b/web/lights/discover.py similarity index 97% rename from lights/discover.py rename to web/lights/discover.py index 6ee6205..99d64f8 100644 --- a/lights/discover.py +++ b/web/lights/discover.py @@ -1,6 +1,6 @@ import socket, json from typing import Dict, Type, List -from protocol import TPLinkSmartHomeProtocol +from .protocol import TPLinkSmartHomeProtocol class Discover: diff --git a/lights/protocol.py b/web/lights/protocol.py similarity index 100% rename from lights/protocol.py rename to web/lights/protocol.py diff --git a/lights/smartbulb.py b/web/lights/smartbulb.py similarity index 96% rename from lights/smartbulb.py rename to web/lights/smartbulb.py index 7e1d6c2..fdf5c00 100644 --- a/lights/smartbulb.py +++ b/web/lights/smartbulb.py @@ -1,4 +1,4 @@ -from protocol import TPLinkSmartHomeProtocol +from .protocol import TPLinkSmartHomeProtocol from typing import Optional, Dict diff --git a/lights/test.py b/web/lights/test.py similarity index 88% rename from lights/test.py rename to web/lights/test.py index ae41e26..b066e6d 100644 --- a/lights/test.py +++ b/web/lights/test.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import time -from discover import Discover -from smartbulb import SmartBulb +from .discover import Discover +from .smartbulb import SmartBulb """ This is a test file to show how things work. diff --git a/web/peripherals.py b/web/peripherals.py new file mode 100644 index 0000000..a93df49 --- /dev/null +++ b/web/peripherals.py @@ -0,0 +1,51 @@ +from lights.smartbulb import SmartBulb +import postgresLibrary as pL +import requests +from flask.json import jsonify +from flask import request, Response + +def handlePeripheral(peripheral, body, routes): + print(peripheral, body, routes) + if len(routes) == 0: + return "No Route to Host." + elif peripheral['type'] == 'thermostat': + dest = routes[0] + url = 'http://' + dest['target'] + '/api/set-temp' + print(url) + + power = "false" + if body['power']: + power = "true" + + pL.updatePeripheralStateAndPower(int(body['state']), body['power'], peripheral['uuid']) + + resp = requests.request( + method='POST', + url=url, + headers= { + 'Content-Type': 'application/json' + }, + data='{ "state": ' + str(int(body['state'])) + ', "power": ' + power + ' }' , + cookies={}, + timeout=0.5, + allow_redirects=False + ) + + response = Response(resp.content, resp.status_code, {}) + return response + + elif peripheral['type'] == 'lightDimmable': + bulb = SmartBulb(routes[0]['target']) + power = body['power'] + brightness = int(body['state']) + + if power: + bulb.state('ON') + bulb.brightness(brightness) + else: + bulb.state('OFF') + + pL.updatePeripheralStateAndPower(brightness, power, peripheral['uuid']) + return "OK" + else: + return "Unknown Type." \ No newline at end of file diff --git a/web/postgresLibrary.py b/web/postgresLibrary.py index cd2ab34..f4f5010 100644 --- a/web/postgresLibrary.py +++ b/web/postgresLibrary.py @@ -26,6 +26,13 @@ def getAll( query ): closeConnection(conn, cur) return values +def getAllParams( query, params ): + conn, cur = setupConnection() + cur.execute(query, params) + values = cur.fetchall() + closeConnection(conn, cur) + return values + def getItembyUUID( query, uuid ): conn, cur = setupConnection() cur.execute(query, (uuid,)) @@ -223,7 +230,7 @@ def getPeripheralbyUUID( periUUID ): def getPeripheralsbyRoom( roomUUID ): """returns information by room_uuid for all peripherals in room""" - query = "SELECT * from peripherals WHERE room_uuid = (%s);" + query = "SELECT * from peripherals WHERE room_uuid = (%s) ORDER BY name DESC;" peripherals = getItemsbyCol(query, roomUUID) return peripherals @@ -253,6 +260,10 @@ def updatePeripheralActive( active, periUUID): query = "UPDATE peripherals SET active = (%s) WHERE uuid = (%s);" return updateItem( query, (active, periUUID)) +def updatePeripheralStateAndPower( state, power, periUUID): + query = "UPDATE peripherals SET state = (%s), power = (%s), last_update = CURRENT_TIMESTAMP WHERE uuid = (%s);" + return updateItem( query, (state, power, periUUID)) + #TV Channel Select Functions def getChannels(): query = "SELECT * from tv_channels;" @@ -464,3 +475,40 @@ def deleteEmployee( empUUID ): def updateEmployeeAdmin( admin, empUUID): query = "UPDATE employees SET admin = (%s) WHERE uuid = (%s);" return updateItem(query, (admin,empUUID)) + +# Get routing table entries for a room / peripheral ID. +def routingEntries ( room_uuid, peripheral_uuid ): + return getAllParams(""" + SELECT * + FROM routing + WHERE + ( + room_uuid = %s OR + peripheral_uuid = %s + ) + AND enabled + ORDER BY metric ASC + """, (room_uuid, peripheral_uuid)) + +# Get routing table entries for a room / peripheral ID. +def roomRoutingEntries ( room_uuid ): + return getAllParams(""" + SELECT * + FROM routing + WHERE + room_uuid = %s + AND enabled + ORDER BY metric ASC + """, (room_uuid,)) + +# Get routing table entries for a room / peripheral ID. +def peripheralRoutingEntries ( peripheral_uuid ): + return getAllParams(""" + SELECT * + FROM routing + WHERE + peripheral_uuid = %s + AND enabled + ORDER BY metric ASC + """, (peripheral_uuid,)) + diff --git a/web/requirements.txt b/web/requirements.txt index 8f02268..d1a904e 100644 --- a/web/requirements.txt +++ b/web/requirements.txt @@ -1,3 +1,4 @@ psycopg2 flask -flask_restful \ No newline at end of file +flask_restful +requests \ No newline at end of file diff --git a/web/thermostat/HT16K33.py b/web/thermostat/HT16K33.py new file mode 100644 index 0000000..7abdc01 --- /dev/null +++ b/web/thermostat/HT16K33.py @@ -0,0 +1,101 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import division + + +# Constants +DEFAULT_ADDRESS = 0x70 +HT16K33_BLINK_CMD = 0x80 +HT16K33_BLINK_DISPLAYON = 0x01 +HT16K33_BLINK_OFF = 0x00 +HT16K33_BLINK_2HZ = 0x02 +HT16K33_BLINK_1HZ = 0x04 +HT16K33_BLINK_HALFHZ = 0x06 +HT16K33_SYSTEM_SETUP = 0x20 +HT16K33_OSCILLATOR = 0x01 +HT16K33_CMD_BRIGHTNESS = 0xE0 + + +class HT16K33(object): + """Driver for interfacing with a Holtek HT16K33 16x8 LED driver.""" + + def __init__(self, address=DEFAULT_ADDRESS, i2c=None, **kwargs): + """Create an HT16K33 driver for device on the specified I2C address + (defaults to 0x70) and I2C bus (defaults to platform specific bus). + """ + if i2c is None: + import Adafruit_GPIO.I2C as I2C + i2c = I2C + self._device = i2c.get_i2c_device(address, **kwargs) + self.buffer = bytearray([0]*16) + + def begin(self): + """Initialize driver with LEDs enabled and all turned off.""" + # Turn on the oscillator. + self._device.writeList(HT16K33_SYSTEM_SETUP | HT16K33_OSCILLATOR, []) + # Turn display on with no blinking. + self.set_blink(HT16K33_BLINK_OFF) + # Set display to full brightness. + self.set_brightness(15) + + def set_blink(self, frequency): + """Blink display at specified frequency. Note that frequency must be a + value allowed by the HT16K33, specifically one of: HT16K33_BLINK_OFF, + HT16K33_BLINK_2HZ, HT16K33_BLINK_1HZ, or HT16K33_BLINK_HALFHZ. + """ + if frequency not in [HT16K33_BLINK_OFF, HT16K33_BLINK_2HZ, + HT16K33_BLINK_1HZ, HT16K33_BLINK_HALFHZ]: + raise ValueError('Frequency must be one of HT16K33_BLINK_OFF, HT16K33_BLINK_2HZ, HT16K33_BLINK_1HZ, or HT16K33_BLINK_HALFHZ.') + self._device.writeList(HT16K33_BLINK_CMD | HT16K33_BLINK_DISPLAYON | frequency, []) + + def set_brightness(self, brightness): + """Set brightness of entire display to specified value (16 levels, from + 0 to 15). + """ + if brightness < 0 or brightness > 15: + raise ValueError('Brightness must be a value of 0 to 15.') + self._device.writeList(HT16K33_CMD_BRIGHTNESS | brightness, []) + + def set_led(self, led, value): + """Sets specified LED (value of 0 to 127) to the specified value, 0/False + for off and 1 (or any True/non-zero value) for on. + """ + if led < 0 or led > 127: + raise ValueError('LED must be value of 0 to 127.') + # Calculate position in byte buffer and bit offset of desired LED. + pos = led // 8 + offset = led % 8 + if not value: + # Turn off the specified LED (set bit to zero). + self.buffer[pos] &= ~(1 << offset) + else: + # Turn on the speciried LED (set bit to one). + self.buffer[pos] |= (1 << offset) + + def write_display(self): + """Write display buffer to display hardware.""" + for i, value in enumerate(self.buffer): + self._device.write8(i, value) + + def clear(self): + """Clear contents of display buffer.""" + for i, value in enumerate(self.buffer): + self.buffer[i] = 0 diff --git a/web/thermostat/SevenSegment.py b/web/thermostat/SevenSegment.py new file mode 100644 index 0000000..ba725b7 --- /dev/null +++ b/web/thermostat/SevenSegment.py @@ -0,0 +1,204 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from . import HT16K33 + + +# Digit value to bitmask mapping: +DIGIT_VALUES = { + ' ': 0x00, + '-': 0x40, + '0': 0x3F, + '1': 0x06, + '2': 0x5B, + '3': 0x4F, + '4': 0x66, + '5': 0x6D, + '6': 0x7D, + '7': 0x07, + '8': 0x7F, + '9': 0x6F, + 'A': 0x77, + 'B': 0x7C, + 'C': 0x39, + 'D': 0x5E, + 'E': 0x79, + 'F': 0x71 +} + +IDIGIT_VALUES = { + ' ': 0x00, + '-': 0x40, + '0': 0x3F, + '1': 0x30, + '2': 0x5B, + '3': 0x79, + '4': 0x74, + '5': 0x6D, + '6': 0x6F, + '7': 0x38, + '8': 0x7F, + '9': 0x7D, + 'A': 0x7E, + 'B': 0x67, + 'C': 0x0F, + 'D': 0x73, + 'E': 0x4F, + 'F': 0x4E +} + + + +class SevenSegment(HT16K33.HT16K33): + """Seven segment LED backpack display.""" + + def __init__(self, invert=False, **kwargs): + """Initialize display. All arguments will be passed to the HT16K33 class + initializer, including optional I2C address and bus number parameters. + """ + super(SevenSegment, self).__init__(**kwargs) + self.invert = invert + + def set_invert(self, _invert): + """Set whether the display is upside-down or not. + """ + self.invert = _invert + + def set_digit_raw(self, pos, bitmask): + """Set digit at position to raw bitmask value. Position should be a value + of 0 to 3 with 0 being the left most digit on the display.""" + if pos < 0 or pos > 3: + # Ignore out of bounds digits. + return + # Jump past the colon at position 2 by adding a conditional offset. + offset = 0 if pos < 2 else 1 + + # Calculate the correct position depending on orientation + if self.invert: + pos = 4-(pos+offset) + else: + pos = pos+offset + + # Set the digit bitmask value at the appropriate position. + self.buffer[pos*2] = bitmask & 0xFF + + def set_decimal(self, pos, decimal): + """Turn decimal point on or off at provided position. Position should be + a value 0 to 3 with 0 being the left most digit on the display. Decimal + should be True to turn on the decimal point and False to turn it off. + """ + if pos < 0 or pos > 3: + # Ignore out of bounds digits. + return + # Jump past the colon at position 2 by adding a conditional offset. + offset = 0 if pos < 2 else 1 + + # Calculate the correct position depending on orientation + if self.invert: + pos = 4-(pos+offset) + else: + pos = pos+offset + + # Set bit 7 (decimal point) based on provided value. + if decimal: + self.buffer[pos*2] |= (1 << 7) + else: + self.buffer[pos*2] &= ~(1 << 7) + + def set_digit(self, pos, digit, decimal=False): + """Set digit at position to provided value. Position should be a value + of 0 to 3 with 0 being the left most digit on the display. Digit should + be a number 0-9, character A-F, space (all LEDs off), or dash (-). + """ + if self.invert: + self.set_digit_raw(pos, IDIGIT_VALUES.get(str(digit).upper(), 0x00)) + else: + self.set_digit_raw(pos, DIGIT_VALUES.get(str(digit).upper(), 0x00)) + + if decimal: + self.set_decimal(pos, True) + + def set_colon(self, show_colon): + """Turn the colon on with show colon True, or off with show colon False.""" + if show_colon: + self.buffer[4] |= 0x02 + else: + self.buffer[4] &= (~0x02) & 0xFF + + def set_left_colon(self, show_colon): + """Turn the left colon on with show color True, or off with show colon + False. Only the large 1.2" 7-segment display has a left colon. + """ + if show_colon: + self.buffer[4] |= 0x04 + self.buffer[4] |= 0x08 + else: + self.buffer[4] &= (~0x04) & 0xFF + self.buffer[4] &= (~0x08) & 0xFF + + def set_fixed_decimal(self, show_decimal): + """Turn on/off the single fixed decimal point on the large 1.2" 7-segment + display. Set show_decimal to True to turn on and False to turn off. + Only the large 1.2" 7-segment display has this decimal point (in the + upper right in the normal orientation of the display). + """ + if show_decimal: + self.buffer[4] |= 0x10 + else: + self.buffer[4] &= (~0x10) & 0xFF + + def print_number_str(self, value, justify_right=True): + """Print a 4 character long string of numeric values to the display. + Characters in the string should be any supported character by set_digit, + or a decimal point. Decimal point characters will be associated with + the previous character. + """ + # Calculate length of value without decimals. + length = sum(map(lambda x: 1 if x != '.' else 0, value)) + # Error if value without decimals is longer than 4 characters. + if length > 4: + self.print_number_str('----') + return + # Calculcate starting position of digits based on justification. + pos = (4-length) if justify_right else 0 + # Go through each character and print it on the display. + for i, ch in enumerate(value): + if ch == '.': + # Print decimal points on the previous digit. + self.set_decimal(pos-1, True) + else: + self.set_digit(pos, ch) + pos += 1 + + def print_float(self, value, decimal_digits=2, justify_right=True): + """Print a numeric value to the display. If value is negative + it will be printed with a leading minus sign. Decimal digits is the + desired number of digits after the decimal point. + """ + format_string = '{{0:0.{0}F}}'.format(decimal_digits) + self.print_number_str(format_string.format(value), justify_right) + + def print_hex(self, value, justify_right=True): + """Print a numeric value in hexadecimal. Value should be from 0 to FFFF. + """ + if value < 0 or value > 0xFFFF: + # Ignore out of range values. + return + self.print_number_str('{0:X}'.format(value), justify_right) diff --git a/web/thermostat/__init__.py b/web/thermostat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/thermostat/server.py b/web/thermostat/server.py new file mode 100644 index 0000000..a5dffba --- /dev/null +++ b/web/thermostat/server.py @@ -0,0 +1,352 @@ +#!/usr/bin/python +from __future__ import division +from flask import Flask +from flask_restful import Resource, Api +from flask import request, Response +from flask.json import jsonify +from flask import render_template +import requests +import json +import uuid +from datetime import date, datetime + + + +# from . import SevenSegment +PORT_NUMBER = 8000 + +app = Flask(__name__) +api = Api(app) + + +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Constants +DEFAULT_ADDRESS = 0x70 +HT16K33_BLINK_CMD = 0x80 +HT16K33_BLINK_DISPLAYON = 0x01 +HT16K33_BLINK_OFF = 0x00 +HT16K33_BLINK_2HZ = 0x02 +HT16K33_BLINK_1HZ = 0x04 +HT16K33_BLINK_HALFHZ = 0x06 +HT16K33_SYSTEM_SETUP = 0x20 +HT16K33_OSCILLATOR = 0x01 +HT16K33_CMD_BRIGHTNESS = 0xE0 + + +class HT16K33(object): + """Driver for interfacing with a Holtek HT16K33 16x8 LED driver.""" + + def __init__(self, address=DEFAULT_ADDRESS, i2c=None, **kwargs): + """Create an HT16K33 driver for device on the specified I2C address + (defaults to 0x70) and I2C bus (defaults to platform specific bus). + """ + if i2c is None: + import Adafruit_GPIO.I2C as I2C + i2c = I2C + self._device = i2c.get_i2c_device(address, **kwargs) + self.buffer = bytearray([0]*16) + + def begin(self): + """Initialize driver with LEDs enabled and all turned off.""" + # Turn on the oscillator. + self._device.writeList(HT16K33_SYSTEM_SETUP | HT16K33_OSCILLATOR, []) + # Turn display on with no blinking. + self.set_blink(HT16K33_BLINK_OFF) + # Set display to full brightness. + self.set_brightness(15) + + def set_blink(self, frequency): + """Blink display at specified frequency. Note that frequency must be a + value allowed by the HT16K33, specifically one of: HT16K33_BLINK_OFF, + HT16K33_BLINK_2HZ, HT16K33_BLINK_1HZ, or HT16K33_BLINK_HALFHZ. + """ + if frequency not in [HT16K33_BLINK_OFF, HT16K33_BLINK_2HZ, + HT16K33_BLINK_1HZ, HT16K33_BLINK_HALFHZ]: + raise ValueError('Frequency must be one of HT16K33_BLINK_OFF, HT16K33_BLINK_2HZ, HT16K33_BLINK_1HZ, or HT16K33_BLINK_HALFHZ.') + self._device.writeList(HT16K33_BLINK_CMD | HT16K33_BLINK_DISPLAYON | frequency, []) + + def set_brightness(self, brightness): + """Set brightness of entire display to specified value (16 levels, from + 0 to 15). + """ + if brightness < 0 or brightness > 15: + raise ValueError('Brightness must be a value of 0 to 15.') + self._device.writeList(HT16K33_CMD_BRIGHTNESS | brightness, []) + + def set_led(self, led, value): + """Sets specified LED (value of 0 to 127) to the specified value, 0/False + for off and 1 (or any True/non-zero value) for on. + """ + if led < 0 or led > 127: + raise ValueError('LED must be value of 0 to 127.') + # Calculate position in byte buffer and bit offset of desired LED. + pos = led // 8 + offset = led % 8 + if not value: + # Turn off the specified LED (set bit to zero). + self.buffer[pos] &= ~(1 << offset) + else: + # Turn on the speciried LED (set bit to one). + self.buffer[pos] |= (1 << offset) + + def write_display(self): + """Write display buffer to display hardware.""" + for i, value in enumerate(self.buffer): + self._device.write8(i, value) + + def clear(self): + """Clear contents of display buffer.""" + for i, value in enumerate(self.buffer): + self.buffer[i] = 0 + + + +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +# Digit value to bitmask mapping: +DIGIT_VALUES = { + ' ': 0x00, + '-': 0x40, + '0': 0x3F, + '1': 0x06, + '2': 0x5B, + '3': 0x4F, + '4': 0x66, + '5': 0x6D, + '6': 0x7D, + '7': 0x07, + '8': 0x7F, + '9': 0x6F, + 'A': 0x77, + 'B': 0x7C, + 'C': 0x39, + 'D': 0x5E, + 'E': 0x79, + 'F': 0x71 +} + +IDIGIT_VALUES = { + ' ': 0x00, + '-': 0x40, + '0': 0x3F, + '1': 0x30, + '2': 0x5B, + '3': 0x79, + '4': 0x74, + '5': 0x6D, + '6': 0x6F, + '7': 0x38, + '8': 0x7F, + '9': 0x7D, + 'A': 0x7E, + 'B': 0x67, + 'C': 0x0F, + 'D': 0x73, + 'E': 0x4F, + 'F': 0x4E +} + + + +class SevenSegment(HT16K33): + """Seven segment LED backpack display.""" + + def __init__(self, invert=False, **kwargs): + """Initialize display. All arguments will be passed to the HT16K33 class + initializer, including optional I2C address and bus number parameters. + """ + super(SevenSegment, self).__init__(**kwargs) + self.invert = invert + + def set_invert(self, _invert): + """Set whether the display is upside-down or not. + """ + self.invert = _invert + + def set_digit_raw(self, pos, bitmask): + """Set digit at position to raw bitmask value. Position should be a value + of 0 to 3 with 0 being the left most digit on the display.""" + if pos < 0 or pos > 3: + # Ignore out of bounds digits. + return + # Jump past the colon at position 2 by adding a conditional offset. + offset = 0 if pos < 2 else 1 + + # Calculate the correct position depending on orientation + if self.invert: + pos = 4-(pos+offset) + else: + pos = pos+offset + + # Set the digit bitmask value at the appropriate position. + self.buffer[pos*2] = bitmask & 0xFF + + def set_decimal(self, pos, decimal): + """Turn decimal point on or off at provided position. Position should be + a value 0 to 3 with 0 being the left most digit on the display. Decimal + should be True to turn on the decimal point and False to turn it off. + """ + if pos < 0 or pos > 3: + # Ignore out of bounds digits. + return + # Jump past the colon at position 2 by adding a conditional offset. + offset = 0 if pos < 2 else 1 + + # Calculate the correct position depending on orientation + if self.invert: + pos = 4-(pos+offset) + else: + pos = pos+offset + + # Set bit 7 (decimal point) based on provided value. + if decimal: + self.buffer[pos*2] |= (1 << 7) + else: + self.buffer[pos*2] &= ~(1 << 7) + + def set_digit(self, pos, digit, decimal=False): + """Set digit at position to provided value. Position should be a value + of 0 to 3 with 0 being the left most digit on the display. Digit should + be a number 0-9, character A-F, space (all LEDs off), or dash (-). + """ + if self.invert: + self.set_digit_raw(pos, IDIGIT_VALUES.get(str(digit).upper(), 0x00)) + else: + self.set_digit_raw(pos, DIGIT_VALUES.get(str(digit).upper(), 0x00)) + + if decimal: + self.set_decimal(pos, True) + + def set_colon(self, show_colon): + """Turn the colon on with show colon True, or off with show colon False.""" + if show_colon: + self.buffer[4] |= 0x02 + else: + self.buffer[4] &= (~0x02) & 0xFF + + def set_left_colon(self, show_colon): + """Turn the left colon on with show color True, or off with show colon + False. Only the large 1.2" 7-segment display has a left colon. + """ + if show_colon: + self.buffer[4] |= 0x04 + self.buffer[4] |= 0x08 + else: + self.buffer[4] &= (~0x04) & 0xFF + self.buffer[4] &= (~0x08) & 0xFF + + def set_fixed_decimal(self, show_decimal): + """Turn on/off the single fixed decimal point on the large 1.2" 7-segment + display. Set show_decimal to True to turn on and False to turn off. + Only the large 1.2" 7-segment display has this decimal point (in the + upper right in the normal orientation of the display). + """ + if show_decimal: + self.buffer[4] |= 0x10 + else: + self.buffer[4] &= (~0x10) & 0xFF + + def print_number_str(self, value, justify_right=True): + """Print a 4 character long string of numeric values to the display. + Characters in the string should be any supported character by set_digit, + or a decimal point. Decimal point characters will be associated with + the previous character. + """ + # Calculate length of value without decimals. + length = sum(map(lambda x: 1 if x != '.' else 0, value)) + # Error if value without decimals is longer than 4 characters. + if length > 4: + self.print_number_str('----') + return + # Calculcate starting position of digits based on justification. + pos = (4-length) if justify_right else 0 + # Go through each character and print it on the display. + for i, ch in enumerate(value): + if ch == '.': + # Print decimal points on the previous digit. + self.set_decimal(pos-1, True) + else: + self.set_digit(pos, ch) + pos += 1 + + def print_float(self, value, decimal_digits=2, justify_right=True): + """Print a numeric value to the display. If value is negative + it will be printed with a leading minus sign. Decimal digits is the + desired number of digits after the decimal point. + """ + format_string = '{{0:0.{0}F}}'.format(decimal_digits) + self.print_number_str(format_string.format(value), justify_right) + + def print_hex(self, value, justify_right=True): + """Print a numeric value in hexadecimal. Value should be from 0 to FFFF. + """ + if value < 0 or value > 0xFFFF: + # Ignore out of range values. + return + self.print_number_str('{0:X}'.format(value), justify_right) + + +segment = SevenSegment(address=0x70) +segment.begin() +segment.clear() + +@app.route('/api/set-temp', methods=['POST']) +def setTemp(): + body = request.get_json() + + if body == None: + return None + + if body['power']: + segment.print_number_str("{} F".format(int(body['state']))) + segment.set_digit_raw(2, 0x63) + segment.write_display() + else: + segment.print_number_str("0FF ") + segment.write_display() + + + return "OK" + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=PORT_NUMBER)