SmartThings Smart Home Dashboard #7

10/15/21: Update to add a link to the GitHub Repository for the code listed below.

This post finally brings us to a fully functional SmartThings Smart Home Dashboard (SHD). There’s still more I’ll be adding, but this post brings home everything we’ve covered in the previous posts in this series and gives us something we can actually start using to monitor and control our Smart Home.

If you have an iOS device, you can save the SHD to your Home Screen and it will look like an app when you run it, without the address bar from the browser. It makes it much cleaner looking and gives you some extra display space.

If you haven’t been following along you will, at a minimum, want to check out the first three posts (Creating a Public URL, Creating a SmartApp Framework, and Configuring Your SmartApp in SmartThings Developer Workspace) and the first part of the fifth post which shows you how to Create a SmartThings Personal Access Token. I’ll try to fill in any other gaps in this post, but if something seems foreign or isn’t working quite right, you may want to review all previous posts in this series.

This series has attempted to take you through all of the steps to create your own SmartThings SHD, each post covering a different element or concept. You are more than welcome to copy the SHD that I have created, but I wanted to try to give you the pieces you need to create your own if you choose to do so.

Just as an overview, once you get everything above setup, the server will automatically retrieve all rooms, devices, presence sensors, and scenes that you have already created in the SmartThings app. You’ll be able to view everything in the smartthings.db, which is automatically created for you once the server starts. It then filters the list of devices based on the capabilities that the SHD supports (see below) and will send them to your browser once you successfully connect and login from your browser. From there, you can optionally configure the data displayed to sort and filter the displayed rooms/devices /capabilities.

I’ll cover the available features of this version of the SHD in the first part of this post, and then provide the environment information and source code in the second.

Supported Devices

This version of the SHD supports the following device capabilities: battery, contactSensor, doorControl, lock, motionSensor, presenceSensor, relativeHumidityMeasurement, switch, switchLevel, temperatureMeasurement, thermostatCoolingSetpoint, thermostatFanMode, thermostatHeatingSetpoint, thermostatMode, and thermostatOperatingState.

Other capabilities can be added as desired, but I’ve found that many capabilities, such as setting bulb colors, are more easily accomplished using scenes and executing them through the SHD.

Authentication

The first thing you’ll notice when you point your browser at your server’s home page is that you have to login. This not only helps prevent unauthorized access to our Smart Home devices, but also identifies the role/permissions of the person logging in.

SHD Login Screen

Once you’ve logged in on a device, you won’t need to login again until either a) you logout, or b) your session expires. You can logout by clicking on your user name on the top left on the SHD. You will be presented with a Logout option. Session life is an app.config setting in st_webapp.py. By default, I’m setting it to 30 days, but you can change that to whatever you want.

Currently we are supporting three different roles: Admin, User, and Guest.

Admin users can not only access the SHD, but will also have access to various Admin-only features. They will be displayed in gold on the SHD homepage.

The User role has full access to the SHD (both inside and outside of the local network). They will be displayed in white on the SHD homepage.

Guest users only have access to the rooms that were granted in our Rooms configuration Admin page. They can see all devices in the rooms they have access to, but can only send commands to the devices granted in the same tool. Guest users also only have access to the SHD while connected to the local network. They will be displayed in light blue on the SHD homepage.

Authorized users are defined on the users table in the users.db database. They can be inserted directly into the table or you can use the Admin – Maintain Users tool, covered below.

Important Information at a Glance

The home view of the SHD shows you everything you need to decide if you need to look any further.

The title bar shows the logged in user, the location, and a refresh button. The location will be displayed in RED if the web socket connection is unavailable, and will turn to WHITE once the connection is reestablished.

It shows all rooms as well as the temperature and/or humidity and device battery status for each room, if it has the respective sensors. The room tile color is an indicator of the status of one or more devices in the room and the border color of the room tile also provides important information – described below. The home view also shows all presence sensors associated with the location.

SHD Home Screen
Rooms

As mentioned above, if a room has a temperature and/or humidity sensor, it will be displayed on the room tile. If the room has multiple such sensors, the last sensor displayed in the room (based on device sequence) will be the one displayed on the room tile.

If there is a door open (either a doorControl sensor, such as a Garage Door Opener, or a contactSensor) or a lock is unlocked, the room tile will be red.

If a switch is turned on in the room, it will illuminate the room tile.

If you have a door open and a light on, the room tile will be red and will be illuminated.

If the room contains one or more items that have a battery sensor, the bottom of the room tile will show the lowest battery level of those devices. The icon will turn from green, to yellow, to red as the battery level drops. Once it reaches a critical level (i.e., < 30%), the room border will be highlighted in red to indicate that attention is needed.

If the room has a motion sensor and motion is detected, the border of the room tile will be highlighted in orange to indicate activity in the room. The same will happen if the room has a thermostat and it is actively heating or cooling.

If the room has a device that has gone offline, the border of the room will turn black to indicate that a device has gone dark. An active device (motion sensor or thermostat) will override an offline device for the period it is active. Once it is no longer active, the border will return to the offline status.

If the room tile is blue and the border is white, everything is secure and no further attention is needed.

Presence Sensors

Presence sensors will be displayed in blue if they are Home and grey if they are Away.

Devices
SHD – Entry Room Devices

You can click on a room tile to see all of the devices associated with the room. Or more appropriately, all of the supported devices associated with the room. Many of the same rules that are applied to room tiles are applied to device tiles.

An unsecured device (doorControl, contactSensor or lock) or an active motion sensor will be be indicated with a red tile.

A switch in an “on” state will be indicated with an illuminated tile.

A thermostat that is actively cooling will be indicated with a light blue tile. When actively heating it will be displayed with a light red tile.

An offline device will be shown in grey with a black border.

The color of the battery icon will indicate its remaining power level.

Many devices will have a default icon to indicate the type of device. For example, lights, switches and bulbs (any device that supports the switch capability), will be represented with a lightbulb icon. The tile will be blue when the switch is off and will be illuminated when the switch is on. If the switch is a dimmer, the illumination level will also be show below the bulb and there will be a slider at the bottom of the tile that will allow you to change the level. Tapping the center of the tile will toggle the state of the switch. Changing the slider while the switch is off will also turn the bulb on when setting the illumination level. The default icon can be changed in the Admin console under the Rooms menu.

Tiles for devices that have no actionable capabilities, such as a motion sensor, contact sensor, temperature sensor, etc., are display only. Tapping the tile will take no action. But all devices will be updated in real-time to display their current state.

Scenes

Choosing the Scenes menu on the navigation bar will display all scenes for the location. Clicking on a scene tile will ask for confirmation to run the scene.

SHD Scenes Screen

Admin Access

If you’re logged in as an Admin, you will have a gear menu on the navigation bar that will give you access to all Admin features.

SHD Admin Home Page

From the Admin Home Page, you can:

Refresh Scenes

If you add, remove or rename a scene in the SmartThings app, this option will update your local database. Updates will be pushed to all dashboards automatically.

Refresh Device Status

If for some reason you have a device that isn’t showing the current status, but it’s showing correctly in the SmartThings app, you can use this command to fetch the current status of all devices. Updates will be pushed to all dashboards automatically.

Refresh Device Health

If for some reason you have a device that is showing a health status (online/offline) that is different than your SmartThings app, you can use this command to fetch the current health status of all devices. Updates will be pushed to all dashboards automatically.

Refresh Foundation Data

If you’ve added, removed or renamed a device or renamed your location, you can use this command to update your database. Updates will be pushed to all dashboards automatically.

Users Menu

SHD Admin – Maintain Users
Maintain Users

Available from the Users menu, you can use this page to add new users or maintain existing users. You can update the user’s name, role, active status, or reset their password.

Inactivating a user will effectively disable their account until they are set back to active again. If they were logged in prior to being inactivated, they will continue to receive broadcasted web socket updates for up to 15 minutes. Web socket pings are done every 15 minutes (by default) from each SHD to make sure the user is still active, and if not their display will be replaced with an unauthorized message. They will also encounter this if they try to initiate any actions after being inactivated, such as toggling a switch or some other device.

Resetting the password simply blanks out the password on the database out so it can be reset the next time the user logs in.

You can change your own name and reset your own password, but it won’t let you change your role or active status so you don’t accidentally lock yourself out.

View/Modify User Logging Settings

Available from the Users menu, this page will allow you to configure which user events will be logged.

View Failed Login Attempts

Available from the Users menu, this page will allow you to view failed login attempts. You can also delete the log from this page.

View User Logs

Available from the Users menu, this page will allow you to view logged user events. You can also delete the log from this page.

Rooms Menu

SHD Admin – Rooms Configuration
Change the Location Display Name

Available from the Rooms menu, this page will allow you to optionally define a Nickname for your location. If a nickname exists, the SHD will use it for the display name. If you delete the nickname, it will revert back to the official location display name. Once you save this change, it will be pushed out to all dashboards automatically.

Associate an Email with the Location

Available from the Rooms menu, this page will allow you to define an email address for the location. This is for future use.

Configure Rooms, Devices and Capabilities

Available from the Rooms menu, this page will allow you to define visible and sequence properties for rooms, devices and capabilities. This will determine what is displayed in the SHD and in what order.

You can also change the default icon for devices by updating the icon field for that specific device. This value must be a Font Awesome icon that we downloaded from the free version (i.e., fas fa-power-off).

Changes made to any of these values will be pushed out to all dashboards automatically.

You can also identify which rooms will be visible to Guest users by setting the Guest Access field at the room-level. Setting Guest Access at the device-level will allow the Guest user to send commands to that device. Devices that are unavailable to the Guest user will be identified with a black border. Changes made to these values will require the dashboard to be reloaded to update the visuals. However, the server will still validate Guest user access prior to executing a command for any device.

Presence Menu

SHD Admin – Presence Configuration

Configure Presence Sensors

Available from the Presence menu, this page will allow you to define a nickname, visible and sequence properties for presence sensors. This will determine what is displayed in the SHD and in what order. Since it’s not as easy to update a presence sensor name as it is for normal devices, I expose a Nickname property in this tool. If a nickname exists, it will be used by the SHD, otherwise it will use the label value for the device. Changes made to these values will be pushed out to all dashboards automatically.

Scenes Menu

SHD Admin – Scenes Configuration
Configure Scenes

Available from the Scenes menu, this page will allow you to define visible and sequence properties for scenes. This will determine what is displayed in the SHD and in what order. Changes made to these values will be pushed out to all dashboards automatically.

You can also identify which scenes will be executable by Guest users by setting the Guest Access field. Scenes that are unavailable to the Guest user will be identified with a black border. Changes made to these values will require the dashboard to be reloaded to take affect, however the server will still validate Guest user access to the scene prior to executing.


The Code…

Setting up the environment

I’m not going to cover the use of a virtual environment to install and run your SHD project, but I would recommend you consider it if you don’t have a dedicated SD card for this project. I will typically do my development in a virtual environment and then once I’m ready to roll it out permanently, I’ll install it on a dedicated SD card. That’s mainly because I haven’t quite figured out how to automate the starting of a virtual environment at boot and then automate the start of my server within the virtual environment. I consider it vital that my server auto-start at boot so I don’t have to connect to get things going because I run my servers headless. If you know how to reliably accomplish this, please let me know!

If you haven’t already done so, you’ll need to install the following packages:

sudo apt-get-update && sudo apt-get-upgrade
sudo pip3 install flask
sudo pip3 install flask-socketio
sudo pip3 install eventlet
sudo pip3 install requests
sudo pip3 install flask-sqlalchemy
sudo pip3 install flask-login

Create the following project folders:

mkdir /home/pi/st_webook
mkdir /home/pi/st_webhook/my_secrets
mkdir /home/pi/st_webhook/templates
mkdir /home/pi/st_webhook/static
mkdir /home/pi/st_webhook/static/css
mkdir /home/pi/smartthings

Once you’ve completed this post, you should have the following folders and files underneath your project folder (/home/pi/st_webhook). Note that users.db will be automatically created once you run the server.

.
├── my_secrets
│   ├── __init__.py
│   └── secrets.py
├── smartthings.py
├── static
│   ├── autorefresh.js
│   ├── css
│   │   └── all.css
│   ├── favicon.png
│   └── webfonts
│       ├── fa-brands-400.eot
│       ├── fa-brands-400.svg
│       ├── fa-brands-400.ttf
│       ├── fa-brands-400.woff
│       ├── fa-brands-400.woff2
│       ├── fa-regular-400.eot
│       ├── fa-regular-400.svg
│       ├── fa-regular-400.ttf
│       ├── fa-regular-400.woff
│       ├── fa-regular-400.woff2
│       ├── fa-solid-900.eot
│       ├── fa-solid-900.svg
│       ├── fa-solid-900.ttf
│       ├── fa-solid-900.woff
│       └── fa-solid-900.woff2
├── st_webhook.py
├── templates
│   ├── admin_base.html
│   ├── admin_failed_login.html
│   ├── admin_home.html
│   ├── admin_logging.html
│   ├── admin_logs.html
│   ├── admin_presence.html
│   ├── admin_rooms.html
│   ├── admin_scenes.html
│   ├── admin_users.html
│   ├── base.html
│   ├── dashboard.html
│   └── login.html
└── users.db

You’ll also have the smartthings.db which will be automatically created once you run the server, but it will be located under a different folder (/home/pi/smartthings). I only did this because I was creating different versions of this project as I progressed through this series and didn’t want to finish with 10 different databases. You should feel free to change the path of STDB in smartthings.py if you wish.

favicon.png

You can copy this image if you want and save it to your static folder as favicon.png.

Downloading Font Awesome

For consistency and performance reasons, I have decided to use the free downloaded version of Font Awesome for my icons. Here’s how you do it…

Go to https://fontawesome.com/v5.15/how-to-use/on-the-web/setup/hosting-font-awesome-yourself and click on the Download Font Awesome Free for the Web link to start the download. Unzip the file and copy the entire webfonts folder to your static folder. Then copy the css/all.css file to your static/css folder. Note: See the above file structure for proper placement of the folder and file.

Note: This is as of September 2021. It’s possible that v5.15 is no longer the current version when you see this, so you may need to go to their home page and navigate to the Hosting Font Awesome Yourself download link.

Secrets

You’ll need to create a secrets.py file if you haven’t already done so.

This file will contain any private information that you don’t want to share with others, such as API keys. If you upload your project to Github, for example, you’ll want to exclude this file so others don’t have access to your private data.

First create a new file named __init__.py and save it in the my_secrets folder. It should be empty with no contents. This just simply tells python this folder is a python package.

Then create a new file named secrets.py. It will have the following contents:

SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXX' #My secret key

ST_WEBHOOK = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' # SmartThings AppID
PA_TOKEN = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' # Personal Access Token with ALL permissions

# Replace with your local and public URLs
CORS_ALLOWED_ORIGINS = ['http://localhost:5000','http://192.168.2.221:5000','http://c5ff-172-13-0-18.ngrok.io','https://c5ff-172-13-0-18.ngrok.io']

If you need to create a SECRET_KEY, open a terminal and start a python session (python3 <ENTER>). Once in python, enter the following commands:

import os
print(os.urandom(64).hex())

That will print a long string of random numbers and characters to your terminal. Copy and paste that into your SECRET_KEY variable above.

You should have your SmartThings AppID (from this post) for the ST_WEBHOOK value and your PA_TOKEN value (from this post).

CORS_ALLOWED_ORIGINS will contain your local IP address as well as your public URL (both http and https). If you don’t have a public URL yet, you’ll need to check out this post.

Save secrets.py in the my_secrets folder.

GitHub Source

I’ve updated this post to add a link to the GitHub repository for the following code.

autorefresh.js

This file will refresh your SHD display if you pull down the screen. It’s the equivalent of clicking on the refresh icon on the top-right of the SHD. It triggers a web socket event that executes our readData(refresh=False) method inside of our SmartThings object (smartthings.py). This will refresh most data from the database and broadcast it to all dashboards. If you make a manual change to the smartthings.db database, this will push those changes out. If you make a change using the Admin console, the refresh is initiated from the save events automatically.

Save this file in your project static folder (i.e., /home/pi/st_webhook/static)

var pStart = {x:0,y:0};
var pStop = {x:0,y:0};

function swipeStart(e) {
   if (typeof e["targetTouches"] !== "undefined") {
      var touch = e.targetTouches[0];
      pStart.x = touch.screenX;
      pStart.y = touch.screenY;
   }
   else {
      pStart.x = e.screenX;
      pStart.y = e.screenY;
   }
}
function swipeEnd(e) {
   if (typeof e["changedTouches"] !== "undefined") {
      var touch = e.changedTouches[0];
      pStop.x = touch.screenX;
      pStop.y = touch.screenY;
   }
   else {
      pStop.x = e.screenX;
      pStop.y = e.screenY;
   }
   swipeCheck();
}
function swipeCheck() {
   var changeX = pStart.x - pStop.x;
   var changeY = pStart.y - pStop.y;
   if (isPullDown(changeY, changeX)) {
      refresh();
   }
}
function isPullDown(dY, dX) {
   return dY < 0 && (
      (Math.abs(dX) <= 100 && Math.abs(dY) >= 300)
      || (Math.abs(dX)/Math.abs(dY) <= 0.3 && dY >= 60)
   );
}
document.addEventListener('touchstart', function(e) { swipeStart(e); }, false);
document.addEventListener('touchend', function(e) { swipeEnd(e); }, false);
smartthings.py

This file contains the SmartThings class and all associated methods. It is responsible for all SmartThings-related interactions (API and database).

Note: I’ve added some additional columns to the smartthings.db in this version. If you’re following along with this series, you can either delete your old database (easiest option) and let the server create the new version with the new columns, or you can issue SQL statements to manually add the new columns to your existing database. If you want to manually add the columns, here’s how you do it:

Open a terminal and change to the directory where your smartthings.db is stored. Then you’ll start a python session to add the new columns.

cd /home/pi/smartthings
python3
>>import sqlite3
>>conn = sqlite3.connect('smartthings.db')
>>c1 = conn.cursor()
>>c1.execute('ALTER TABLE device ADD COLUMN guest_access INTEGER')
>>c1.execute('ALTER TABLE device ADD COLUMN nickname TEXT')
>>c1.execute('ALTER TABLE device ADD COLUMN icon TEXT')
>>conn.commit()
>>c1.execute('ALTER TABLE room ADD COLUMN guest_access INTEGER')
>>conn.commit()
>>c1.execute('ALTER TABLE scene ADD COLUMN guest_access INTEGER')
>>conn.commit()
>>exit()

Save this file in your project folder (i.e., /home/pi/st_webhook).

#My Secrets
from my_secrets.secrets import ST_WEBHOOK, PA_TOKEN

#HTTP Libs
import requests

#JSON Libs
import json

#sqlite3 Libs
import sqlite3

#os Libs
from os.path import exists

#datetime Libs
from datetime import datetime

HOME_URL = 'https://api.smartthings.com/v1/'
APP_HEADERS = {'Authorization': 'Bearer ' + PA_TOKEN}  # Use this header when you don't have an authToken being passed in

STDB = '/home/pi/smartthings/smartthings.db'  #Path to SmartThings DB - It's best to use the full path.

# This is the list of supported capabilities and attributes.  Add to this list as you add more support.  This helps keep your JSON payload smaller.
DEV_LIST = [('presenceSensor', 'presence'), ('battery', 'battery'), ('switch', 'switch'), ('switchLevel', 'level'),
	('doorControl', 'door'), ('lock', 'lock'), ('temperatureMeasurement', 'temperature'),
	('relativeHumidityMeasurement', 'humidity'), ('contactSensor', 'contact'), ('motionSensor', 'motion'),
	('thermostatCoolingSetpoint', 'coolingSetpoint'), ('thermostatOperatingState', 'thermostatOperatingState'),
	('thermostatFanMode', 'thermostatFanMode'), ('thermostatHeatingSetpoint', 'heatingSetpoint'), ('thermostatMode', 'thermostatMode')]

# This just gives us a list of supported capabilities (the first item in each tuple in DEV_LIST) that we can use to test against later
CAP_LIST = [cap[0] for cap in DEV_LIST]

class SmartThings:

	def __init__(self, location_id=''): #Pass a location_id if you have multiple locations
		self.location_id = location_id
		self.app_id = ''
		self.installed_app_id = ''
		self.app_name = ''
		self.configuration_id = ''
		self.name = ''
		self.location = {'location': {'locationId' : '', 'name' : ''}, 'presence':[], 'rooms' : [], 'scenes': []}

	def initialize(self, refresh=True):
		#  This creates and seeds the database, if needed, and updates the database with device status
		#  and builds out the self.location JSON structure with the data to pass to our HTML.
		if not self.location_id:
			self.getInstalledApps()
		if not exists(STDB):
			self.createDB()
			self.loadData()
		self.readData(refresh)

	def getInstalledApps(self):
		#If you only have one location, this will read by AppID to get the installed location_id for you.
		fullURL = HOME_URL + 'installedapps?appid=' + ST_WEBHOOK
		headers = APP_HEADERS
		r = requests.get(fullURL, headers=headers)
		print('Get Installed Apps: %d' % r.status_code)
		if r.status_code == 200:
			data = json.loads(r.text)
			self.location_id = data['items'][0]['locationId']

	def createDB(self):
		# Create our smartthings.db
		conn = sqlite3.connect(STDB)
		cursor = conn.cursor()

		create_location_table = '''CREATE TABLE IF NOT EXISTS location(
			location_id TEXT NOT NULL PRIMARY KEY,
			name TEXT NOT NULL,
			nickname TEXT,
			latitude TEXT,
			longitude TEXT,
			time_zone_id TEXT,
			email TEXT
			)'''
		cursor.execute(create_location_table)

		create_app_table = '''CREATE TABLE IF NOT EXISTS app(
			location_id TEXT NOT NULL,
			app_id TEXT NOT NULL,
			installed_app_id TEXT,
			display_name TEXT,
			configuration_id TEXT,
			PRIMARY KEY (location_id, app_id),
			FOREIGN KEY (location_id)
			REFERENCES location (location_id)
			ON DELETE CASCADE
			ON UPDATE CASCADE
			)'''
		cursor.execute(create_app_table)

		create_scene_table = '''CREATE TABLE IF NOT EXISTS scene(
			scene_id TEXT NOT NULL PRIMARY KEY,
			name TEXT NOT NULL,
			location_id TEXT NOT NULL,
			visible INTEGER,
			seq INTEGER,
			guest_access INTEGER,
			FOREIGN KEY (location_id)
			REFERENCES location (location_id)
			ON DELETE CASCADE
			ON UPDATE CASCADE
			)'''
		cursor.execute(create_scene_table)

		create_room_table = '''CREATE TABLE IF NOT EXISTS room(
			location_id TEXT NOT NULL,
			room_id TEXT NOT NULL,
			name TEXT NOT NULL,
			visible INTEGER,
			seq INTEGER,
			guest_access INTEGER,
			PRIMARY KEY (room_id)
			FOREIGN KEY (location_id)
			REFERENCES location (location_id)
			ON DELETE CASCADE
			ON UPDATE CASCADE
			)'''
		cursor.execute(create_room_table)

		create_device_table = '''CREATE TABLE IF NOT EXISTS device(
			location_id TEXT NOT NULL,
			room_id TEXT NOT NULL,
			device_id TEXT NOT NULL,
			presentation_id TEXT,
			name TEXT,
			health TEXT,
			label TEXT,
			category TEXT,
			device_type_name TEXT,
			visible INTEGER,
			seq INTEGER,
			guest_access INTEGER,
			nickname TEXT,
			icon TEXT,
			PRIMARY KEY (device_id),
			FOREIGN KEY (room_id)
			REFERENCES room (room_id)
			ON DELETE CASCADE
			ON UPDATE CASCADE
			)'''
		cursor.execute(create_device_table)

		create_capability_table = '''CREATE TABLE IF NOT EXISTS capability(
			location_id TEXT NOT NULL,
			device_id TEXT NOT NULL,
			capability_id TEXT NOT NULL,
			visible INTEGER,
			state TEXT,
			seq INTEGER,
			updated TEXT,
			PRIMARY KEY (device_id, capability_id),
			FOREIGN KEY (device_id)
			REFERENCES device (device_id)
			ON DELETE CASCADE
			ON UPDATE CASCADE
			)'''
		cursor.execute(create_capability_table)

		conn.commit()
		conn.close()

	def loadData(self):
		# Load seed data into the database
		status = False
		if self.loadLocation():
			if self.loadAppConfig():
				if self.loadRooms():
					if self.loadDevices():
						print('Data Loaded...')
						status = True
					else:
						print('Failed loading devices.')
				else:
					print('Failed loading rooms.')
			else:
				print('Failed loading App Config.')
		else:
			print(f'Failed loading location ({self.location_id}).')
		return status
		

	def readData(self, refresh=True):
		# Update device status/health and scenes and build the self.location JSON for the browser
		status = False
		if self.readLocation():
			if self.readAppConfig():
				if self.readRooms():
					if self.readDevices():
						if refresh:
							if self.loadAllDevicesStatus():
								if self.loadAllDevicesHealth():
									if self.loadAllScenes():
										status = True
									else:
										print('Failed loading scenes.')
								else:
									print('Failed loading All Devices Health.')
							else:
								print('Failed loading All Devices Status.')
						if self.readAllScenes():
							print('Data Read...')
							status = True
						else:
							print('Failed reading scenes.')
					else:
						print('Failed reading devices.')
				else:
					print('Failed reading rooms.')
			else:
				print('Failed reading App Config.')
		else:
			print(f'Failed reading location ({self.location_id}).')
		#print(self.location)
		return status

	def loadLocation(self):
		#This will give you all of the location related data and populate the database.
		status = False
		fullURL = HOME_URL + 'locations/' + self.location_id
		headers = APP_HEADERS
		r = requests.get(fullURL, headers=headers)
		print('Get Location: %d' % r.status_code)
		if r.status_code == 200:
			data = json.loads(r.text)
			conn = sqlite3.connect(STDB)
			cursor = conn.cursor()
			update_location = 'update location set name=?, latitude=?, longitude=?, time_zone_id=? where location_id=?'
			update_values = (data['name'], data['latitude'], data['longitude'], data['timeZoneId'], self.location_id)
			cursor.execute(update_location, update_values)
			if cursor.rowcount == 0:
				insert_location = 'insert into location (location_id, name, nickname, latitude, longitude, time_zone_id, email) values(?,?,?,?,?,?,?)'
				insert_values = (self.location_id, data['name'], '', data['latitude'], data['longitude'], data['timeZoneId'], '')
				cursor.execute(insert_location, insert_values)
			conn.commit()
			conn.close()
			status = True
		return status

	def readLocation(self):
		#This will read location data from the database and populate self.location
		status = False
		conn = sqlite3.connect(STDB)
		cursor = conn.cursor()
		for row in cursor.execute('select * from location where location_id=?', (self.location_id,)):
			location_id, name, nickname, latitude, longitude, time_zone, email = row
			self.name = name
			self.display_name = nickname if len(nickname) > 0 else name
			self.latitude = latitude
			self.longitude = longitude
			self.location = {'location': {'locationId' : location_id, 'name' : self.display_name, 'latitude' : latitude, 'longitude' : longitude, 'timeZoneId' : time_zone, 'email' : email}, 'presence':[], 'rooms' : []}
			status = True
		conn.close()
		return status

	def loadAppConfig(self):
		#This tree will give you all of the information about your app, including the installedAppId, displayName, description, configurationId,
		#  and all configuration data.
		#  If you're only interested in the devices selected during user install/update, you can get all of that info here.
		status = False
		fullURL = HOME_URL + 'installedapps?locationId=' + self.location_id + '&appId=' + ST_WEBHOOK
		headers = APP_HEADERS
		r = requests.get(fullURL, headers=headers)
		print('Get installedAppId: %d' % r.status_code)
		if r.status_code == 200:
			print('Config - App *****************************************\n')
			#print(r.text)
			listData = json.loads(r.text)
			installedAppId = ''
			displayName = ''
			configurationId = ''
			for item in listData['items']:
				if item['appId'] == ST_WEBHOOK and item['installedAppStatus'] == 'AUTHORIZED':
					installedAppId = item['installedAppId']
					displayName = item['displayName']
					self.installed_app_id = installedAppId
			baseURL = HOME_URL + 'installedapps/' + installedAppId
			endURL = '/configs'
			headers = APP_HEADERS
			fullURL = baseURL + endURL
			r = requests.get(fullURL, headers=headers)
			print('Get configurationId: %d' % r.status_code)
			if r.status_code == 200:
				print('Config ID *****************************************\n')
				#print(r.text)
				appData = json.loads(r.text)
				conn = sqlite3.connect(STDB)
				cursor = conn.cursor()
				for item in appData['items']:
					if item['configurationStatus'] == 'AUTHORIZED':
						configurationId = item['configurationId']
						insert_app = 'insert or replace into app values(?,?,?,?,?)'
						app_values = (self.location_id, ST_WEBHOOK, installedAppId, displayName, configurationId)
						cursor.execute(insert_app, app_values)
						conn.commit()
						self.app_name = displayName
						self.configuration_id = configurationId
						fullURL = fullURL + '/' + configurationId
						r = requests.get(fullURL, headers=headers)
						print('Get appConfig: %d' % r.status_code)
						if r.status_code == 200:
							status = True
							print('Config Data *****************************************\n')
							print(r.text)
		conn.close()
		return status

	def readAppConfig(self):
		status = False
		conn = sqlite3.connect(STDB)
		cursor = conn.cursor()
		for row in cursor.execute('select installed_app_id, display_name from app where app_id=? and location_id=?', (ST_WEBHOOK, self.location_id)):
			self.installed_app_id =row[0]
			self.app_name = row[1]
			status = True
		conn.close()
		return status

	def loadRooms(self):
		#This will return all rooms at this location and populate the database.
		status = False
		baseURL = HOME_URL + 'locations/'
		endURL = '/rooms'
		headers = APP_HEADERS
		fullURL = baseURL + self.location_id + endURL
		r = requests.get(fullURL, headers=headers)
		print('Get Rooms: %d' % r.status_code)
		if r.status_code == 200:
			data = json.loads(r.text)
			conn = sqlite3.connect(STDB)
			cursor = conn.cursor()
			roomRows = []
			for row in cursor.execute('select room_id from room where location_id=?', (self.location_id,)):
				roomRows.append(row[0])
			update_room = 'update room set name=? where room_id=?'
			insert_room = 'insert into room (location_id, room_id, name, visible, seq) values(?,?,?,?,?)'
			for rm in data['items']:
				update_values = (rm['name'], rm['roomId'])
				cursor.execute(update_room, update_values)
				if cursor.rowcount == 1:
					roomRows.remove(rm['roomId'])
				else:
					insert_values = (self.location_id, rm['roomId'], rm['name'], 1, 99)
					cursor.execute(insert_room, insert_values)
				conn.commit()
			for roomId in roomRows:
				cursor.execute('update room set visible=? where room_id=?', (0, roomId))
				conn.commit()
			conn.close()
			status = True
		else:
			print('Get Rooms Failed.  Status: %s' % r.status_cd)
		return status

	def readRooms(self):
		#Load all room data from the database.
		status = False
		conn = sqlite3.connect(STDB)
		cursor = conn.cursor()

		self.location['rooms'] = []
		
		for row in cursor.execute('select * from room where location_id=? and visible=?', (self.location_id,1)):
			location_id, room_id, name, visible_val, seq, guest_access = row
			room = {'roomId' : room_id, 'name' : name, 'seq': seq, 'guest_access': guest_access, 'devices' : []}
			self.location['rooms'].append(room)
			status = True
		conn.close()
		return status

	def loadDevices(self):
		#This will give us all devices at this location, but we have to put them into room groupings or 
		#  presenceSensor groupings.  Stores the data in the database.
		status = False
		baseURL = HOME_URL + 'devices'
		endURL = '?locationId=' + self.location_id
		headers = APP_HEADERS
		fullURL = baseURL + endURL
		r = requests.get(fullURL, headers=headers)
		print('Get Devices: %d' % r.status_code)
		if r.status_code == 200:
			data = json.loads(r.text)
			conn = sqlite3.connect(STDB)
			cursor = conn.cursor()
			deviceRows = []
			for row in cursor.execute('select device_id from device where location_id=?', (self.location_id,)):
				deviceRows.append(row[0])
			update_device = 'update device set room_id=?, name=?, label=?, category=?, device_type_name=? where device_id=?'
			insert_device = 'insert into device (location_id,room_id,device_id,presentation_id,name,health,label,category,device_type_name,visible,seq,guest_access,nickname,icon) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
			insert_capability = 'insert into capability (location_id, device_id, capability_id, visible, state, seq, updated) values(?,?,?,?,?,?,?)'
			for dev in data['items']:
				dtn = dev.get('dth','')
				if dtn:
					dtn = dtn.get('deviceTypeName', '')
				update_device_values = (dev.get('roomId', 0), dev['name'], dev['label'], dev['components'][0]['categories'][0]['name'], dtn, dev['deviceId'])
				cursor.execute(update_device, update_device_values)
				if cursor.rowcount == 1:
					deviceRows.remove(dev['deviceId'])
				else:
					device_insert_values = (self.location_id, dev.get('roomId', 0), dev['deviceId'], dev.get('presentationId',''), dev['name'], '?', dev['label'], dev['components'][0]['categories'][0]['name'], dtn, 1, 99, 0, '', '')
					cursor.execute(insert_device, device_insert_values)
					for comp in dev['components']:
						for cap in comp['capabilities']:
							capability_values = (self.location_id, dev['deviceId'], cap['id'], 1, '', 99, '')
							cursor.execute(insert_capability, capability_values)
				status = True
				conn.commit()
			for deviceId in deviceRows:
				cursor.execute('update device set visible=? where device_id=?', (0, deviceId))
				conn.commit()
			conn.close()
		return status

	def readDevices(self):
		# Reads device data from the database.
		status = False
		conn = sqlite3.connect(STDB)
		cursor = conn.cursor()
		c2 = conn.cursor()
		self.location['presence'] = []
		for room in self.location['rooms']:
			room['devices'] = []
		for row in cursor.execute('select * from device where location_id=? and visible=?', (self.location_id,1)):
			d_location_id, d_room_id, d_device_id, d_presentation_id, d_name, d_health, d_label, d_category, d_device_type, d_visible, d_seq, d_guest_access, d_nickname, d_icon = row
			device = {'deviceId' : d_device_id, 'name' : d_name, 'label' : d_nickname if d_nickname else d_label, 'seq': d_seq, 'health': d_health, 
				'guest_access': d_guest_access, 'icon': d_icon, 'capabilities' : []}
			for r2 in c2.execute('select * from capability where device_id=? and visible=?', (d_device_id, 1)):
				status = True
				c_location_id, c_device_id, c_capability_id, c_visible, c_state, c_seq, dt = r2
				if c_capability_id in CAP_LIST:
					capability = {'id' : c_capability_id, 'state' : c_state, 'seq': c_seq, 'updated': dt}
					device['capabilities'].append(capability)
			if len(device['capabilities']) > 0 and (d_room_id == 0 or d_room_id == '0'):
				self.location['presence'].append(device)
			else:
				if len(device['capabilities']) > 0:
					for room in self.location['rooms']:
						if room['roomId'] == d_room_id:
							room['devices'].append(device)

		conn.close()
		return status

	def loadAllDevicesStatus(self):
		#We spin through each device to get the current status of all of it's capabilities.
		#  This data gets written to the database and updates self.location.
		status = False
		baseURL = HOME_URL + 'devices/'
		headers = APP_HEADERS
		endURL = '/status'

		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		dt = datetime.now().strftime('%m/%d/%y %H:%M:%S')

		for pres in self.location['presence']:
			fullURL = baseURL + pres['deviceId'] + endURL
			r = requests.get(fullURL, headers=headers)
			if r.status_code == 200:
				print('Device Loaded: %s' % pres['label'])
				data = json.loads(r.text)

				main = dict(data.get('components','')).get('main','')
				if main:
					for dev in DEV_LIST:
						cap = dict(main.get(dev[0],'')).get(dev[1],'')
						if cap:
							for capability in pres['capabilities']:
								if capability['id'] == dev[0]:
									capability['state'] = cap['value']
									capability['updated'] = dt
									c1.execute('update capability set state=?, updated=? where device_id=? and capability_id=?', 
										(cap['value'], dt, pres['deviceId'], dev[0]))
									conn.commit()

		for room in self.location['rooms']:
			for device in room['devices']:
				fullURL = baseURL + device['deviceId'] + endURL
				r = requests.get(fullURL, headers=headers)
				if r.status_code == 200:
					print('Device Loaded: %s' % device['label'])
					data = json.loads(r.text)

					main = dict(data.get('components','')).get('main','')
					if main:
						for dev in DEV_LIST:
							cap = dict(main.get(dev[0],'')).get(dev[1],'')
							if cap:
								for capability in device['capabilities']:
									if capability['id'] == dev[0]:
										capability['state'] = cap['value']
										c1.execute('update capability set state=?, updated=? where device_id=? and capability_id=?', 
											(cap['value'], dt, device['deviceId'], dev[0]))
										conn.commit()
										status = True
		conn.close()
		return status

	def loadAllDevicesHealth(self):
		#Here we spin through all devices to get it's current health status (online/offline).
		#  This data gets written to the database and updates self.location.
		status = False
		baseURL = HOME_URL + 'devices/'
		headers = APP_HEADERS
		endURL = '/health'

		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()

		for pres in self.location['presence']:
			deviceId = pres['deviceId']
			fullURL = baseURL + str(deviceId) + endURL
			r = requests.get(fullURL, headers=headers)
			if r.status_code == 200:
				status = True
				data = json.loads(r.text)
				print('Get Presence Health: %s - %s' % (data['state'], pres['label']))
				pres['health'] = data['state']
				c1.execute('update device set health=? where device_id=?', (data['state'], deviceId))
				conn.commit()

		for rm in self.location['rooms']:
			for dev in rm['devices']:
				deviceId = dev['deviceId']
				fullURL = baseURL + deviceId + endURL
				r = requests.get(fullURL, headers=headers)
				if r.status_code == 200:
					status = True
					data = json.loads(r.text)
					print('Get Device Health: %s - %s' % (data['state'], dev['label']))
					dev['health'] = data['state']
					c1.execute('update device set health=? where device_id=?', (data['state'], deviceId))
					conn.commit()
		conn.close()
		return status

	def updateDeviceHealth(self, deviceId, status):
		#This gets called when a device health event fires.
		#  It updates the database and self.location.
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		
		for pres in self.location['presence']:
			for dev in pres['devices']:
				if dev['deviceId'] == deviceId:
					dev['health'] = status
					c1.execute('update device set health=? where device_id=?', (status, deviceId))
					conn.commit()
					conn.close()
					return True

		for room in self.location['rooms']:
			for dev in room['devices']:
				if dev['deviceId'] == deviceId:
					dev['health'] = status
					c1.execute('update device set health=? where device_id=?', (status, deviceId))
					conn.commit()
					conn.close()
					return True
		return False


	def loadAllScenes(self):
		#This will load all scenes and must be filtered for the location and writes them to the database.
		status = False
		baseURL = HOME_URL + 'scenes'
		headers = APP_HEADERS
		fullURL = baseURL
		r = requests.get(fullURL, headers=headers)
		
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		
		print(f'loadAllScenes() r.status_code: {r.status_code}')
		if r.status_code == 200:
			status = True
			data = json.loads(r.text)
			#print(f'*****Scenes\nr.text\n******')
			self.location['scenes'] = []
			sceneRows = []
			for row in c1.execute('select scene_id from scene where location_id=?', (self.location_id,)):
				sceneRows.append(row[0])
			print('Starting sceneRows: %s' % sceneRows)
			for scene in data['items']:
				if scene['locationId'] == self.location_id:
					scene_data = {'sceneId': scene['sceneId'], 'sceneName': scene['sceneName']}
					self.location['scenes'].append(scene_data)

					c1.execute('update scene set name=? where scene_id=?', (scene['sceneName'], scene['sceneId']))
					if c1.rowcount == 0:
						c1.execute('insert into scene (scene_id,name,location_id,visible,seq) values (?,?,?,?,?)',
						(scene['sceneId'], scene['sceneName'], self.location_id, 1, 99))
					else:
						sceneRows.remove(scene['sceneId'])
					# ~ c1.execute('insert or replace into scene values (?,?,?,?,?)', 
						# ~ (scene['sceneId'], scene['sceneName'], self.location_id, 1, 99))
					conn.commit()
			print('Ending sceneRows: %s' % sceneRows)
			for sceneId in sceneRows:
				c1.execute('delete from scene where scene_id=?', (sceneId,))
				conn.commit()
		conn.close()
		return status

	def readAllScenes(self):
		#Reads scenes from the database.
		status = False

		conn = sqlite3.connect(STDB)
		conn.row_factory = sqlite3.Row
		c1 = conn.cursor()
		self.location['scenes'] = []
		for scene in c1.execute('select * from scene where location_id=? and visible=?', (self.location_id,1)):
			self.location['scenes'].append(dict(scene))
			status = True
		return status

	def updateDevice(self, deviceId, capability, attribute, value):
		#This is called when a device event occurs.  It updates the database and self.location data 
		#  and then returns the values to be emitted to the browsers.
		print('Updating: %s / %s / %s / %s' % (deviceId, capability, attribute, value))

		emit_data = True
		emit_val = ()
		
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		dt = datetime.now().strftime('%m/%d/%y %H:%M:%S')

		if capability == 'presenceSensor':
			for pres in self.location['presence']:
				if pres['deviceId'] == deviceId:
					for cap in pres['capabilities']:
						if cap['id'] == capability:
							cap['state'] = value

							c1.execute('update capability set state=?, updated=? where device_id=? and capability_id=?',
								(value, dt, deviceId, capability))
								
							conn.commit()
							print(self.location['presence'])
							dev_json = json.dumps({'deviceId': deviceId,'capability': capability, 'value': value})
							emit_val = ('presence_chg', dev_json)
		else:
			for rm in self.location['rooms']:
				for dev in rm['devices']:
					if dev['deviceId'] == deviceId:
						for cap in dev['capabilities']:
							if cap['id'] == capability:
								if cap['state'] == value:
									emit_data = False
								cap['state'] = value
								if emit_data:
									
									c1.execute('update capability set state=?, updated=? where device_id=? and capability_id=?',
										(value, dt, deviceId, capability))
										
									conn.commit()									
									dev_json = json.dumps({'deviceId': deviceId,'capability': capability, 'value': value})
									emit_val = ('device_chg', dev_json)
		conn.close()
		return emit_val

	def deleteSubscriptions(self, authToken, appID):
		#Deletes all subscriptions.
		baseURL = HOME_URL + 'installedapps/'
		headers = {'Authorization': 'Bearer ' + authToken}
		endURL = '/subscriptions'

		r = requests.delete(baseURL + str(appID) + endURL, headers=headers)

		if r.status_code == 200:
			return True

		return False

	def deviceHealthSubscriptions(self, authToken, locationID, appID):
		#Subscribes to device health changes.
		baseURL = HOME_URL + 'installedapps/'
		headers = {'Authorization': 'Bearer ' + authToken}
		endURL = '/subscriptions'
		fullURL = baseURL + str(appID) + endURL

		datasub = {
			'sourceType':'DEVICE_HEALTH',
			'deviceHealth': {
				'locationId':locationID,
				'subscriptionName':'deviceHealthSubscription'
				}
			}
		r = requests.post(fullURL, headers=headers, json=datasub)
		print('Device Health Subscription: %d' % r.status_code)
		if r.status_code == 200:
			return True

		return False

	def capabilitySubscriptions(self, authToken, locationID, appID, capability, attribute, subName, stateChangeOnly=True):
		#Subscribes to specific capability status changes.
		baseURL = HOME_URL + 'installedapps/'
		headers = {'Authorization': 'Bearer ' + authToken}
		endURL = '/subscriptions'
		fullURL = baseURL + str(appID) + endURL

		datasub = {
			'sourceType':'CAPABILITY',
			'capability': {
				'locationId':locationID,
				'capability':capability,
				'attribute':attribute,
				'value':'*',
				'stateChangeOnly':stateChangeOnly,
				'subscriptionName':subName
				}
			}
		r = requests.post(fullURL, headers=headers, json=datasub)
		print('Capability Subscription [%s / %s]: %d' % (capability, attribute, r.status_code))
		if r.status_code == 200:
			return True

		return False

	def deviceSubscriptions(self, authToken, appID, deviceID, capability, attribute, subName):
		#Subscribes to device-specific events.
		baseURL = HOME_URL + 'installedapps/'
		headers = {'Authorization': 'Bearer ' + authToken}
		endURL = '/subscriptions'
		fullURL = baseURL + str(appID) + endURL

		datasub = {
			'sourceType':'DEVICE',
			'device': {
				'deviceId':deviceID,
				'componentId':'*',
				'capability':capability,
				'attribute':'*',
				'value':'*',
				'stateChangeOnly':True,
				'subscriptionName':subName
				}
			}
		r = requests.post(fullURL, headers=headers, json=datasub)
		print('Device Subscription: %d' % r.status_code)
		if r.status_code == 200:
			return True

		return False

	def changeDevice(self, deviceId, capability, value, user=None):
		#This is called when a user requests to change a device state.
		#  It calls an API which, if successful, will trigger a subsequent device event.
		if user and user.role == 'Guest':
			conn = sqlite3.connect(STDB)
			c1 = conn.cursor()
			for row in c1.execute('select guest_access from device where device_id=?', (deviceId,)):
				print('device: %s' % row[0])
			conn.close()
			if not row[0] or row[0] != 1:
				print('Guest not allowed to run this device!')
				return False
		baseURL = HOME_URL + 'devices/'
		headers = APP_HEADERS
		endURL = '/commands'
		fullURL = baseURL + str(deviceId) + endURL
		if capability == 'switchLevel':
			datasub = {
				'commands': [ {
					'component':'main',
					'capability':capability,
					'command':'setLevel',
					'arguments': [
						int(value)
					]
				}
			  ]
			}
		else:
			datasub = {
				'commands': [ {
					'component':'main',
					'capability':capability,
					'command':value
					}
				]
			}
		r = requests.post(fullURL, headers=headers, json=datasub)
		print('Change Device: %d' % r.status_code)
		print (r.text)
		if r.status_code == 200:
			return True

		return False

	def changeThermostat(self, settings, user=None):
		#This is called when a user requests to change a thermostat.
		if user and user.role == 'Guest':
			conn = sqlite3.connect(STDB)
			c1 = conn.cursor()
			for row in c1.execute('select guest_access from device where device_id=?', (settings['deviceId'],)):
				print('device: %s' % row[0])
			conn.close()
			if not row[0] or row[0] != 1:
				print('Guest not allowed to change thermostat!')
				return False
		baseURL = HOME_URL + 'devices/'
		headers = APP_HEADERS
		endURL = '/commands'
		fullURL = baseURL + settings['deviceId'] + endURL

		commandsPayload = []
		for command in settings['commands']:
			arguments = []
			if command['capability'] == 'thermostatHeatingSetpoint':
				cmd = 'setHeatingSetpoint'
				arguments.append(int(command['value']))
			elif command['capability'] == 'thermostatCoolingSetpoint':
				cmd = 'setCoolingSetpoint'
				arguments.append(int(command['value']))
			else:
				cmd = command['value']
			commandsPayload.append({'component':'main', 'capability':command['capability'], 'command':cmd, 'arguments':arguments})

		datasub = {
			'commands': commandsPayload
		}
		print(datasub)
		
		r = requests.post(fullURL, headers=headers, json=datasub)
		print('Change Thermostat: %d' % r.status_code)
		print (r.text)
		if r.status_code == 200:
			return True

		return False

	def runScene(self, scene_id, user=None):
		# Execute a scene
		print(f'Running scene: {scene_id}')
		if user and user.role == 'Guest': # If user is a Guest, make sure they have access first
			conn = sqlite3.connect(STDB)
			c1 = conn.cursor()
			for row in c1.execute('select guest_access from scene where scene_id=?', (scene_id,)):
				print('scene: %s' % row[0])
			conn.close()
			if not row[0] or row[0] != 1:
				print('Guest not allowed to run this scene!')
				return False
		fullURL = HOME_URL + 'scenes/' + scene_id + '/execute'
		headers = APP_HEADERS
		r = requests.post(fullURL, headers=headers)
		print(f'r.status_code: {r.status_code}')
		if (r.status_code == 200):
			return True
		return False

	def getConfig(self):
		# Get Location and Room-Level configs.  Used by Admin console.
		config = {'location': {}, 'rooms': []}
					
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		c2 = conn.cursor()
		c3 = conn.cursor()
		
		for loc in c1.execute('select location_id, name, nickname, email from location where location_id=?', (self.location_id,)):
			newLocation = {'location_id': loc[0], 'name': loc[1], 'nickname': loc[2], 'email': loc[3]}
		config['location'] = newLocation
		
		for rm in c1.execute('select room_id, name, seq, visible, guest_access from room where location_id=?', (self.location_id,)):
			newRoom = {'room_id': rm[0], 'name': rm[1], 'seq': rm[2], 'visible': rm[3], 'guest_access': 0 if not rm[4] else rm[4], 'devices': []}
			for dev in c2.execute('select device_id, label, seq, visible, guest_access, icon from device where room_id=?', (rm[0],)):
				newDevice = {'device_id': dev[0], 'label': dev[1], 'seq': dev[2], 'visible': dev[3], 'guest_access': 0 if not dev[4] else dev[4], 'icon': dev[5] if dev[5] else '', 'capabilities': []}
				for cap in c3.execute('select capability_id, seq, visible from capability where device_id=?', (dev[0],)):
					if cap[0] in CAP_LIST:
						newCapability = {'capability_id': cap[0], 'seq': cap[1], 'visible': cap[2]}
						newDevice['capabilities'].append(newCapability)
				if len(newDevice['capabilities']) > 0:
					newRoom['devices'].append(newDevice)
			if len(newRoom['devices']) > 0:
				config['rooms'].append(newRoom)
		conn.close()
		return config
		
	def updateConfigs(self, configData):
		# Update location and room configs.
		status = False
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		location_id = ''
		nickname = ''
		email = ''
		if len(configData['location']) > 0:
			for item in configData['location']:
				if item.get('location_id', ''):
					location_id = item['location_id']
				elif item.get('nickname',''):
					nickname = item['nickname']
				elif item.get('email',''):
					email = item['email']
			print('nickname: %s / email: %s' % (nickname, email))
			c1.execute('update location set nickname=?, email=? where location_id=?', (nickname, email, location_id))
			status = True
		print('Room items: %d' % len(configData['rooms']))
		for room in configData['rooms']:
			print(room)
			room_id = room.get('room_id', '')
			seq = room.get('seq', 99)
			visible = room.get('visible', 1)
			guest_access = room.get('guest_access', 0)
			c1.execute('update room set seq=?, visible=?, guest_access=? where room_id=?', (seq, visible, guest_access, room_id))
			status = True
		print('Device items: %d' % len(configData['devices']))
		for device in configData['devices']:
			print(device)
			device_id = device.get('device_id', '')
			seq = device.get('seq', 99)
			visible = device.get('visible', 1)
			guest_access = device.get('guest_access', 0)
			icon = device.get('icon', '')
			c1.execute('update device set seq=?, visible=?, guest_access=?, icon=? where device_id=?', (seq, visible, guest_access, icon, device_id))
			status = True
		print('Capability items: %d' % len(configData['capabilities']))
		for capability in configData['capabilities']:
			print(capability)
			device_id = capability.get('device_id', '')
			capability_id = capability.get('capability_id', '')
			seq = capability.get('seq', 99)
			visible = capability.get('visible', 1)
			print(f'seq={seq}, visible={visible}, device_id={device_id}, capability_id={capability_id}')
			c1.execute('update capability set seq=?, visible=? where device_id=? and capability_id=?', (seq, visible, device_id, capability_id))
			status = True
		conn.commit()
		conn.close()
		return status
		
	def getPresence(self):
		# Get Presence configs.  Used by Admin console.
		config = {'presence': []}
		
		conn = sqlite3.connect(STDB)
		conn.row_factory = sqlite3.Row
		c1 = conn.cursor()

		for sensor in c1.execute('select device_id, label, seq, visible, nickname from device where location_id=? and category=?', (self.location_id, 'MobilePresence')):
			config['presence'].append(dict(sensor))
		conn.close()
		return config
		
	def updatePresenceConfigs(self, configData):
		# Update Presence configs.
		status = False
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		if len(configData['presence']) > 0:
			for sensor in configData['presence']:
				print(f'Updating {sensor["device_id"]}')
				c1.execute('update device set nickname=?, seq=?, visible=? where device_id=?', 
					(sensor['nickname'], sensor['seq'], sensor['visible'], sensor['device_id']))
				status = True
			conn.commit()
		conn.close()
		return status
	
	def getScenes(self):
		# Get Scene-level configs.  Used by Admin console.
		config = {'scenes': []}
		
		conn = sqlite3.connect(STDB)
		conn.row_factory = sqlite3.Row
		c1 = conn.cursor()
		
		for scene in c1.execute('select * from scene where location_id=?', (self.location_id,)):
			sceneRecord = dict(scene)
			if sceneRecord['guest_access'] is None:
				sceneRecord['guest_access'] = 0
			config['scenes'].append(sceneRecord)
		return config
		
	def updateSceneConfigs(self, configData):
		# Update scene configs.
		status = False
		conn = sqlite3.connect(STDB)
		c1 = conn.cursor()
		if len(configData['scenes']) > 0:
			for scene in configData['scenes']:
				print(f'Updating {scene["scene_id"]}: visible: {scene["visible"]}')
				c1.execute('update scene set seq=?, visible=?, guest_access=? where scene_id=?', 
					(scene['seq'], scene['visible'], scene['guest_access'], scene['scene_id']))
				status = True
			conn.commit()
		conn.close()
		return status


if __name__ == '__main__': #This will only be True if we are directly running this file for testing.
	st = SmartThings()

	#  If multiple locations associated with account, use this instead #
	#
	#location_id = 'xxxxxxxxxxxxxxxx' # copy value from incoming request
	#st = SmartThings(location_id)

	st.initialize()
	print('*******************************\n\n')
	print(st.location)
st_webhook.py

This is our SmartThings Webhook SmartApp and our web server.

SmartThings will call it as events are triggered (i.e., a switch was turned on), and it will call SmartThings (via our SmartThings object) to execute commands (i.e., turn on a switch).

We will call it from our browser to display our SHD and to execute commands from the SHD.

Save this file in your project folder (i.e., /home/pi/st_webook)

#!/usr/bin/env python

#eventlet WSGI server
import eventlet
eventlet.monkey_patch()

#Flask Libs
from flask import Flask, abort, request, jsonify, render_template, send_from_directory, session, redirect, url_for, flash

#Flask Login Libs
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.urls import url_parse
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user, login_user, logout_user

#Web Sockets
from flask_socketio import SocketIO, send, emit, join_room, leave_room, disconnect, rooms

#HTTP Libs
import requests

#JSON Libs
import json

#datetime
from datetime import datetime, timedelta

#My Libs
from smartthings import SmartThings
from my_secrets.secrets import SECRET_KEY, ST_WEBHOOK, CORS_ALLOWED_ORIGINS


# Replace the second item with your local IP address info
LOCAL_NETWORK_IP = ['127.0.0.1', '192.168.2.']

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins=CORS_ALLOWED_ORIGINS)
app.config['SECRET_KEY'] = SECRET_KEY

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db' # Defines our flask-login user database
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Mute flask-sqlalchemy warning message
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) # Flask session expiration
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=30) # Remember Me cookie expiration (Not sure this works???)
app.config['REMEMBER_COOKIE_SECURE'] = None # Change to True if you want to force using HTTPS to store cookies.
app.config['REMEMBER_COOKIE_HTTPONLY'] = True # Prevents cookies from being accessed on the client-side.

db = SQLAlchemy(app) # This gives us our database/datamodel object

login_manager = LoginManager(app) # This creates our login manager object
login_manager.login_view = 'login' # Defines our login view (basically calls url_for('login'))

class User(UserMixin, db.Model): # This is our User class/model.  It will store our valid users.
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
    active = db.Column(db.Boolean) # This value must be True (1) before the user can login.
    email = db.Column(db.String(100), unique=True) # This is our username.  Must be unique.
    password = db.Column(db.String(100))
    name = db.Column(db.String(1000))
    role = db.Column(db.String(25))
    # db.relationship defines the one-to-many relationship with the UserLogin class/table and can be accessible here (but we won't use it that way)
    #   backref tells sqlalchemy that we can also go from UserLogin to User
    #   lazy='dynamic' tells sqlalchemy not to automatically load the related data into the logins attribute.  It could get large.
    logins = db.relationship('UserLogin', backref='users', lazy='dynamic')

class UserLogin(db.Model): # This is our UserLogin class/model.  It will store login related data for our users
    __tablename__ = 'user_login'
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
    event = db.Column(db.String(50))
    date = db.Column(db.String(100))
    ip = db.Column(db.String(50))

class FailedLogin(db.Model): # This is our FailedLogin class/model.  It will store failed login attempts.
    __tablename__ = 'failed_login'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(100))
    password = db.Column(db.String(100))
    date = db.Column(db.String(100))
    ip = db.Column(db.String(50))

class UserLogging(db.Model): # This is our UserLogging class/model.  It will allow us to turn logging on/off by login-type.
    __tablename__ = 'user_logging'
    id = db.Column(db.Integer, primary_key=True)
    event = db.Column(db.String(50), unique=True)
    log_event = db.Column(db.Boolean, server_default='True')

db.create_all() # Creates our database and tables as defined in the above classes.

if not UserLogging.query.filter(UserLogging.event == 'login').first():
    log = UserLogging(event = 'login', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'logout').first():
    log = UserLogging(event = 'logout', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'connect').first():
    log = UserLogging(event = 'connect', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'disconnect').first():
    log = UserLogging(event = 'disconnect', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'config-view').first():
    log = UserLogging(event = 'config-view', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'config-update').first():
    log = UserLogging(event = 'config-update', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'presence-update').first():
    log = UserLogging(event = 'presence-update', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'scene-update').first():
    log = UserLogging(event = 'scene-update', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'user-update').first():
    log = UserLogging(event = 'user-update', log_event = True)
    db.session.add(log)
    db.session.commit()
if not UserLogging.query.filter(UserLogging.event == 'log-delete').first():
    log = UserLogging(event = 'log-delete', log_event = True)
    db.session.add(log)
    db.session.commit()

# Create first user if it doesn't already exist.  Notice it doesn't have to be a valid email.  It basically serves as our username.
if not User.query.filter(User.email == 'jeff@example.com').first():
    user = User(
        active=True,
        email='jeff@example.com',
        password=generate_password_hash('Password', method='sha256'), # We don't store the actual password, just the hash.
        name='Jeff',
        role='Admin'
    )
    db.session.add(user)
    db.session.commit()


@login_manager.user_loader # This is the login manager user loader.  Used to load current_user.
def load_user(user_id):
    # since the user_id is the primary key of our user table, use it in the query for the user
    user = User.query.get(int(user_id))
    if user and user.active and len(user.password) > 0: # Only return the user if they are active
        return user
    return None


@socketio.on('connect')
def socket_connect():
    # Make sure the current_user is still authenticated.
    if current_user.is_authenticated:
        data = json.dumps({'status': 'connected'})
        emit('conn', data, broadcast=False)
        location_data = json.dumps(st.location)
        room = st.location_id
        print('Joining room: %s' % room)
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        print('ip: %s' % ip)
        if UserLogging.query.filter(UserLogging.event == 'connect').filter(UserLogging.log_event == True).first():
            current_user.logins.append(UserLogin(event='connect', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
            db.session.commit()
        join_room(room)
        emit('location_data', location_data, broadcast=False) #We only need to send this to the user currently connecting, not all.
    else:
        print('Current user no longer authenticated!')
        emit('location_data', '', broadcast=False)  # Send an empty event to notify browser user is no longer authorized

@socketio.on('disconnect')
def socket_disconnect():
    if current_user.is_authenticated:
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        if UserLogging.query.filter(UserLogging.event == 'disconnect').filter(UserLogging.log_event == True).first():
            current_user.logins.append(UserLogin(event='disconnect', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
            db.session.commit()

@socketio.on('pingBack')
def socket_pingback():
    if current_user.is_authenticated:
        emit('pingRcv');
    else:
        print('Current user no longer authenticated!')
        emit('location_data', '', broadcast=False)  # Send an empty event to notify browser user is no longer authorized

@socketio.on('disconn')
def socket_disconn():
    print('Disconnecting unauthorized user! [user: %s]' % User.query.get(int(session['_user_id'])).email)
    try: # Wrapped in a try in case the current_user is no longer active.
        user = User.query.get(int(session['_user_id']))
        if user: # Record the web socket disconnect.  Remove if desired.
            if request.headers.getlist('X-Forwarded-For'):
                ip = request.headers.getlist('X-Forwarded-For')[0]
            else:
                ip = request.remote_addr
            if UserLogging.query.filter(UserLogging.event == 'disconnect').filter(UserLogging.log_event == True).first():
                user.logins.append(UserLogin(event='disconnect', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
                db.session.commit()
    except:
        pass
    disconnect()

@socketio.on('refresh')
def socket_refresh():
    # Make sure the current_user is still authenticated.
    if current_user.is_authenticated:
        if st:
            st.readData(refresh=False)
            location_data = json.dumps(st.location)
            emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.
        else:
            print('st object not defined!')
    else:
        print('Current user no longer authenticated! [user_id: %s]' % session['_user_id'])
        emit('location_data', '', broadcast=False)  # Send an empty event to notify browser user is no longer authorized

@socketio.on('update-device')
def socket_update_device(msg):
    # Make sure the current_user is still authenticated.
    if current_user.is_authenticated:
        if st:
            print('update-device: %s' % msg)
            st.changeDevice(msg['deviceId'], msg['capability'], msg['state'], current_user)
        else:
            print('st object not defined!')
    else:
        print('Current user no longer authenticated! [user_id: %s]' % session['_user_id'])
        emit('location_data', '', broadcast=False)  # Send an empty event to notify browser user is no longer authorized

@socketio.on('update-thermostat')
def socket_update_thermostat(msg):
    # Make sure the current_user is still authenticated.
    if current_user.is_authenticated:
        if st:
            print('update-thermostat: %s' % msg)
            st.changeThermostat(msg, current_user)
        else:
            print('st object not defined!')
    else:
        print('Current user no longer authenticated! [user_id: %s]' % session['_user_id'])
        emit('location_data', '', broadcast=False)  # Send an empty event to notify browser user is no longer authorized

@socketio.on('run-scene')
def socket_run_scene(msg):
    if current_user.is_authenticated:
        if st:
            print('run-scene: %s' % msg)
            if not st.runScene(msg['scene_id'], current_user):
                print('Failed running scene!')
        else:
            print('st object not defined!')
    else:
        print('Current user no longer authenticated! [user_id: %s]' % session['_user_id'])
        emit('location_data', '', broadcast=False)  # Send an empty event to notify browser user is no longer authorized

# This is the login route.  If a users tries to go directly to any URL that requires a login (has the @login_required decorator)
#   before being authenticated, they will be redirected to this URL.  This is defined in the login_manager.login_view setting above.
@app.route('/login', methods=['GET'])
def login():
    if current_user.is_authenticated: # No need to have a logged in user login again.
        return redirect(url_for('index'))
    # The 'next' query parameter will be set automatically by the login_manager
    #   if the user tried to go directly to @login_required URL before authenticating.
    next_page = request.args.get('next')
    print('next: %s' % next_page)
    if not next_page or url_parse(next_page).netloc != '':  # If there is no next query parameter, default to index.
        next_page = url_for('index')
    return render_template('login.html', next_page=next_page)

@app.route('/login', methods=['POST']) # The browser user click the Login button...
def login_post():
    email = request.form.get('email')
    password = request.form.get('password')
    remember = True if request.form.get('remember') else False

    user = User.query.filter_by(email=email).filter_by(active=True).first() # Let's see if this user exists...
    print('User: %s' % user)

    # Capture the IP address so we can check Guest users and log it...
    if request.headers.getlist('X-Forwarded-For'):
        ip = request.headers.getlist('X-Forwarded-For')[0]
    else:
        ip = request.remote_addr

    if user and user.role == 'Guest': # If this is a Guest user, make sure they are logging in from the local network only...
        if LOCAL_NETWORK_IP[0] in ip or LOCAL_NETWORK_IP[1] in ip:
            print(f'Guest User [{user.name}] is connected to local network.  Allowed...')
        else:
            print(f'Guest User [{user.name}] is NOT connected to local network.  Aborting...')
            flash(f'Guest Users Must Be Connected to Local Network')
            return redirect(url_for('login')) # If not, send them back to the Login page.

    # If the user exists in the db, but the password is empty, then take the entered password, hash it, and update the db.
    #   This is how I add a new user to the db without setting the password for them.
    if user and user.password == '':
        print('Setup user!')
        user.password=generate_password_hash(password, method='sha256')
        db.session.commit()

    # Check if the user actually exists and is active
    # Take the user-supplied password, hash it, and compare it to the hashed password in the database
    if not user or not user.active or not check_password_hash(user.password, password):
        # If there's a problem, create a FailedLogin event.
        failed_user = FailedLogin(email=email, password=password, date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip)
        db.session.add(failed_user)
        db.session.commit()

        flash('Please check your login details and try again.')
        return redirect(url_for('login')) # if the user doesn't exist or password is wrong, reload the page

    try: # This just captures the last login for the user in case we decide to use it later.
         # We wrap it in a try in case there was no previous login.
        userLogin = UserLogin.query.filter_by(user_id=user.id).filter_by(event='login').order_by(UserLogin.date.desc()).first()
        print("Last Login: %s" % userLogin.date)
    except:
        pass

    # If the above check passes, then we know the user has the right credentials
    login_user(user, remember=remember)
    session.permanent = True # This is the flask session.  It's set to permanent, but the PERMANENT_SESSION_LIFETIME is applied for expiration.

    # Record the login event.  Remove if desired.
    if UserLogging.query.filter(UserLogging.event == 'login').filter(UserLogging.log_event == True).first():
        user.logins.append(UserLogin(event='login', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()

    # This is the next query parameter that we passed through from the login GET request.
    #  If it was set, we want to now redirect the user to the URL they originally tried to go to.
    next_page = request.args.get('next')
    print('next: %s' % next_page)
    if not next_page or url_parse(next_page).netloc != '':
        next_page = url_for('index')
    return redirect(next_page)

@app.route('/logout')
def logout():
    if current_user.is_authenticated:  # If the user is logged in, record the event and log them out.
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        if UserLogging.query.filter(UserLogging.event == 'logout').filter(UserLogging.log_event == True).first():
            current_user.logins.append(UserLogin(event='logout', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
            db.session.commit()
        logout_user()
    return redirect(url_for('login')) # Logged in or not, redirect to the login page.

# Admin Home Page
@app.route('/admin')
@login_required
def admin():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    if UserLogging.query.filter(UserLogging.event == 'config-view').filter(UserLogging.log_event == True).first():
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        current_user.logins.append(UserLogin(event='config-view', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()
    return render_template('admin_home.html')

# Admin View User Logs
@app.route('/admin-view-logs')
@login_required
def admin_view_logs():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    results = UserLogin.query.all()
    logData = {'logs': []}
    for log in results:
        email = User.query.get(log.user_id).email
        logData['logs'].append({'id': log.id, 'user_id': log.user_id, 'email': email, 'event': log.event, 'date': log.date, 'ip': log.ip})
    return render_template('admin_logs.html', logData=logData)

# Admin Delete User Logs
@app.route('/delete-user-logs', methods=['POST'])
@login_required
def admin_delete_logs():
    print('delete-user-logs')
    if current_user.role != 'Admin':
        return 'Fail', 403
    logData = request.get_json()
    print('logData: %s' % logData)
    for log in logData['logs']:
        logRecord = UserLogin.query.get(log['id'])
        if logRecord:
            db.session.delete(logRecord)
            db.session.commit()
    if UserLogging.query.filter(UserLogging.event == 'log-delete').filter(UserLogging.log_event == True).first():
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        current_user.logins.append(UserLogin(event='log-delete', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()
    return 'OK', 200

# Admin Failed Logins
@app.route('/admin-failed-logins')
@login_required
def admin_failed_logins():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    results = FailedLogin.query.all()
    logData = {'data': []}
    for data in results:
        logData['data'].append({'id': data.id, 'email': data.email, 'password': data.password, 'date': data.date, 'ip': data.ip})
    return render_template('admin_failed_login.html', logData=logData)

# Admin Delete Failed Login
@app.route('/delete-failed-login', methods=['POST'])
@login_required
def admin_delete_failed_login():
    print('delete-failed-login')
    if current_user.role != 'Admin':
        return 'Fail', 403
    logData = request.get_json()
    print('logData: %s' % logData)
    for log in logData['logs']:
        logRecord = FailedLogin.query.get(log['id'])
        if logRecord:
            db.session.delete(logRecord)
            db.session.commit()
    if UserLogging.query.filter(UserLogging.event == 'log-delete').filter(UserLogging.log_event == True).first():
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        current_user.logins.append(UserLogin(event='log-delete', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()
    return 'OK', 200

# Admin Configure Logging
@app.route('/admin-logging')
@login_required
def admin_logging():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    results = UserLogging.query.all()
    logData = {'logs': []}
    for log in results:
        logData['logs'].append({'id': log.id, 'event': log.event, 'log_event': '1' if log.log_event else '0'})
    return render_template('admin_logging.html', logData=logData)

# Admin Updating Logging
@app.route('/update-logging', methods=['POST'])
@login_required
def update_logging():
    if current_user.role != 'Admin':
        return 'Fail', 403
    print('update-logging')
    logData = request.get_json()
    print(logData)
    for log in logData['logs']:
        print('id: %s' % log['id'])
        logRecord = UserLogging.query.get(int(log['id']))
        if logRecord:
            logRecord.log_event = True if log['log_event'] == '1' else False
            db.session.commit()
    if UserLogging.query.filter(UserLogging.event == 'config-update').filter(UserLogging.log_event == True).first():
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        current_user.logins.append(UserLogin(event='config-update', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()
    return 'OK', 200

# Admin Maintain Users
@app.route('/admin-users')
@login_required
def admin_users():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    results = User.query.all()
    userData = {"users": []}
    for user in results:
        userData['users'].append({"id": user.id, "name": user.name, "email": user.email, "role": user.role, "active": 1 if user.active else 0})
    return render_template('admin_users.html', userData=userData)

@app.route('/update-users', methods=['POST'])
@login_required
def update_users():
    if current_user.role != 'Admin':
        return 'Fail', 403
    print('update-users')
    userData = request.get_json()
    print(userData)
    for user in userData['users']:
        print('updating user: %s' % user['id'])
        userRecord = User.query.get(int(user['id']))
        if userRecord:
            userRecord.name = user['name']
            userRecord.role = user['role']
            userRecord.active = True if user['active'] == '1' else False
            if user['reset'] == '1':
                userRecord.password = ''
            db.session.commit()
    if UserLogging.query.filter(UserLogging.event == 'user-update').filter(UserLogging.log_event == True).first():
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        current_user.logins.append(UserLogin(event='user-update', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()
    return 'OK', 200

@app.route('/new-user', methods=['POST'])
@login_required
def new_user():
    if current_user.role != 'Admin':
        return 'Fail', 403
    print('new-user')
    userData = request.get_json()
    print(userData)
    if not User.query.filter(User.email == userData['email']).first():
        user = User(
            active=True if userData['active'] == '1' else False,
            email=userData['email'],
            password='',
            name=userData['name'],
            role=userData['role']
        )
        db.session.add(user)
        db.session.commit()
    if UserLogging.query.filter(UserLogging.event == 'user-update').filter(UserLogging.log_event == True).first():
        if request.headers.getlist('X-Forwarded-For'):
            ip = request.headers.getlist('X-Forwarded-For')[0]
        else:
            ip = request.remote_addr
        current_user.logins.append(UserLogin(event='user-update', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
        db.session.commit()
    return 'OK', 200

# Admin Presence Sensor Config
@app.route('/config-presence')
@login_required
def config_presence():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    configData = st.getPresence()
    return render_template('admin_presence.html', configData=configData)

@app.route('/update-presence-configs', methods=['POST'])
@login_required
def update_presence_configs():
    if current_user.role == 'Admin':
        print('update-presence-configs')
        configData = request.get_json()
        print(configData)
        if st.updatePresenceConfigs(configData):
            st.readData(refresh=False)
            location_data = json.dumps(st.location)
            socketio.emit('location_data', location_data, room=st.location_id) #Broadcase any changes to all users.
            if UserLogging.query.filter(UserLogging.event == 'presence-update').filter(UserLogging.log_event == True).first():
                if request.headers.getlist('X-Forwarded-For'):
                    ip = request.headers.getlist('X-Forwarded-For')[0]
                else:
                    ip = request.remote_addr
                current_user.logins.append(UserLogin(event='presence-update', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
                db.session.commit()
            return 'OK', 200
        return 'Fail', 200
    return 'Fail', 403

# Admin Scenes Config
@app.route('/config-scenes')
@login_required
def config_scenes():
    if current_user.role != 'Admin':
        return redirect(url_for('index'))
    configData = st.getScenes()
    return render_template('admin_scenes.html', configData=configData)

@app.route('/update-scene-configs', methods=['POST'])
@login_required
def update_scene_configs():
    if current_user.role == 'Admin':
        print('update-scene-configs')
        configData = request.get_json()
        print(configData)
        print('Scene items: %d' % len(configData['scenes']))
        if st.updateSceneConfigs(configData):
            st.readData(refresh=False)
            location_data = json.dumps(st.location)
            socketio.emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.
            if UserLogging.query.filter(UserLogging.event == 'scene-update').filter(UserLogging.log_event == True).first():
                if request.headers.getlist('X-Forwarded-For'):
                    ip = request.headers.getlist('X-Forwarded-For')[0]
                else:
                    ip = request.remote_addr
                current_user.logins.append(UserLogin(event='scene-update', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
                db.session.commit()
            return 'OK', 200
        return 'Fail', 200
    return 'Fail', 403

# To access this page, the user must be logged in and also have an Admin role.
@app.route('/config-rooms')
@login_required
def config_rooms():
    if current_user.role == 'Admin':
        configData = st.getConfig()
        return render_template('admin_rooms.html', configData=configData)
    # If the user isn't an Admin, log them out, flash them a message, and send back to the login page.
    logout_user()
    flash("You must be an Administrator to access this page!")
    return redirect(url_for('login'))

# Obviously, we want to make sure the user is logged in here and is an admin.
@app.route('/update-room-configs', methods=['POST'])
@login_required
def update_room_configs():
    if current_user.role == 'Admin':
        print('update-room-configs')
        configData = request.get_json()
        print(configData)
        print('Location items: %d' % len(configData['location']))
        if st.updateConfigs(configData):
            st.readData(refresh=False)
            location_data = json.dumps(st.location)
            socketio.emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.
            if UserLogging.query.filter(UserLogging.event == 'config-update').filter(UserLogging.log_event == True).first():
                if request.headers.getlist('X-Forwarded-For'):
                    ip = request.headers.getlist('X-Forwarded-For')[0]
                else:
                    ip = request.remote_addr
                current_user.logins.append(UserLogin(event='config-update', date=datetime.now().strftime('%m/%d/%y %H:%M:%S'), ip=ip))
                db.session.commit()
            return 'OK', 200
        return 'Fail', 200
    return 'Fail', 403

# Admin Refresh Scenes
@app.route('/admin-refresh-scenes', methods=['POST'])
@login_required
def admin_refresh_scenes():
    if current_user.role != 'Admin':
        return 'Fail', 403
    if st.loadAllScenes():
        if st.readAllScenes():
            location_data = json.dumps(st.location)
            socketio.emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.            
            return 'OK', 200
    return 'Fail', 200

# Admin Refresh All Devices Status
@app.route('/admin-refresh-device-status', methods=['POST'])
@login_required
def admin_refresh_device_status():
    if current_user.role != 'Admin':
        return 'Fail', 403
    if st.loadAllDevicesStatus():
        location_data = json.dumps(st.location)
        socketio.emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.            
        return 'OK', 200
    return 'Fail', 200

# Admin Refresh All Devices Health
@app.route('/admin-refresh-device-health', methods=['POST'])
@login_required
def admin_refresh_device_health():
    if current_user.role != 'Admin':
        return 'Fail', 403
    if st.loadAllDevicesHealth():
        location_data = json.dumps(st.location)
        socketio.emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.            
        return 'OK', 200
    return 'Fail', 200

# Admin Refresh Foundation Data (App, Location, Rooms, Devices)
@app.route('/admin-refresh-foundation', methods=['POST'])
@login_required
def admin_refresh_foundation():
    if current_user.role != 'Admin':
        return 'Fail', 403
    if st.loadData():
        if st.readData(refresh=False):
            location_data = json.dumps(st.location)
            socketio.emit('location_data', location_data, room=st.location_id) #Broadcast any changes to all users.            
            return 'OK', 200        
    return 'Fail', 200

# Only logged in users can see the dashboard.
@app.route('/', methods=['GET'])
@login_required
def index():
    return render_template('dashboard.html')

@app.route('/', methods=['POST'])
def smarthings_requests():
    content = request.get_json()
    print('AppId: %s\nLifeCycle: %s' % (content['appId'], content['lifecycle']))

    if (content['lifecycle'] == 'PING'):
        print('PING: %s' % content)
        challenge = content['pingData']['challenge']
        data = {'pingData':{'challenge': challenge}}
        return jsonify(data)

    elif (content['lifecycle'] == 'CONFIRMATION'):
        confirmationURL = content['confirmationData']['confirmationUrl']
        r = requests.get(confirmationURL)
        print('CONFIRMATION\nContent: %s\nURL: %s\nStatus: %s' % (content,confirmationURL,r.status_code))
        if r.status_code == 200:
            return r.text
        else:
            abort(r.status_code)

    elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'INITIALIZE'):
        print(content['configurationData']['phase'])

        if content['appId'] == ST_WEBHOOK:
            data = {
                      "configurationData": {
                        "initialize": {
                          "name": "ST Test Webhook App",
                          "description": "ST Test Webhook App",
                          "id": "st_webhook_app_page_1",
                          "permissions": [
                            "r:devices:*"
                          ],
                          "firstPageId": "1"
                        }
                      }
                    }
        else:
            data = {'appId':'Not Recognized'}
            print('Initialize Unknown appId: %s' % content['appId'])

        return jsonify(data)

    elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'PAGE'):
        print(content['configurationData']['phase'])
        pageId = content['configurationData']['pageId']

        if content['appId'] == ST_WEBHOOK:
            data = {
                      "configurationData": {
                        "page": {
                          "pageId": "1",
                          "name": "Select Devices",
                          "nextPageId": "null",
                          "previousPageId": "null",
                          "complete": "true",
                          "sections": [
                            {
                              "name": "When this opens/closes...",
                              "settings": [
                                {
                                  "id": "contactSensor",
                                  "name": "Which contact sensor?",
                                  "description": "Tap to set",
                                  "type": "DEVICE",
                                  "required": "true",
                                  "multiple": "false",
                                  "capabilities": [
                                    "contactSensor"
                                  ],
                                  "permissions": [
                                    "r"
                                  ]
                                }
                              ]
                            },
                            {
                              "name": "Turn on/off this light...",
                              "settings": [
                                {
                                  "id": "lightSwitch",
                                  "name": "Which switch?",
                                  "description": "Tap to set",
                                  "type": "DEVICE",
                                  "required": "true",
                                  "multiple": "false",
                                  "capabilities": [
                                    "switch"
                                  ],
                                  "permissions": [
                                    "r",
                                    "x"
                                  ]
                                }
                              ]
                            }
                          ]
                        }
                      }
                    }
        else:
            data = {'appId':'Not Recognized'}
            print('Page Unknown appId: %s' % content['appId'])

        return jsonify(data)

    elif (content['lifecycle'] == 'INSTALL'):
        print(content['lifecycle'])
        data = {'installData':{}}
        resp = content['installData']

        if content['appId'] == ST_WEBHOOK:
            print('Installing ST Webhook')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'switch', 'switch', 'capSwitchSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'lock', 'lock', 'capLockSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'temperatureMeasurement', 'temperature', 'capTempSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'relativeHumidityMeasurement', 'humidity', 'capHumiditySubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'doorControl', 'door', 'capDoorSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'contactSensor', 'contact', 'capContactSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'motionSensor', 'motion', 'capMotionSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'switchLevel', 'level', 'capSwitchLevelSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'battery', 'battery', 'capBatterySubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'presenceSensor', 'presence', 'capPresenceSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatOperatingState', 'thermostatOperatingState', 'capOperatingStateSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatMode', 'thermostatMode', 'capModeSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatCoolingSetpoint', 'coolingSetpoint', 'capCoolSetpointSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatHeatingSetpoint', 'heatingSetpoint', 'capHeatSetpointSubscription')
            st.deviceHealthSubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'])
        else:
            data = {'appId':'Not Recognized'}
            print('Install Unknown appId: %s' % content['appId'])

        return jsonify(data)

    elif (content['lifecycle'] == 'UPDATE'):
        print(content['lifecycle'])
        data = {'updateData':{}}
        resp = content['updateData']
        print('resp: %s' % resp)

        if content['appId'] == ST_WEBHOOK:
            print('Updating ST Webhook')
            st.deleteSubscriptions(resp['authToken'], resp['installedApp']['installedAppId'])
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'switch', 'switch', 'capSwitchSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'lock', 'lock', 'capLockSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'temperatureMeasurement', 'temperature', 'capTempSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'relativeHumidityMeasurement', 'humidity', 'capHumiditySubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'doorControl', 'door', 'capDoorSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'contactSensor', 'contact', 'capContactSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'motionSensor', 'motion', 'capMotionSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'switchLevel', 'level', 'capSwitchLevelSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'battery', 'battery', 'capBatterySubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'presenceSensor', 'presence', 'capPresenceSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatOperatingState', 'thermostatOperatingState', 'capOperatingStateSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatMode', 'thermostatMode', 'capModeSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatCoolingSetpoint', 'coolingSetpoint', 'capCoolSetpointSubscription')
            st.capabilitySubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'thermostatHeatingSetpoint', 'heatingSetpoint', 'capHeatSetpointSubscription')
            st.deviceHealthSubscriptions(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'])
        else:
            data = {'appId':'Not Recognized'}
            print('Update Unknown appId: %s' % content['appId'])

        return jsonify(data)

    elif (content['lifecycle'] == 'OAUTH_CALLBACK'):
        print(content['lifecycle'])
        data = {'oAuthCallbackData':{}}
        return jsonify(data)

    elif (content['lifecycle'] == 'EVENT'):
        data = {'eventData':{}}

        event = content['eventData']['events'][0]

        if content['appId'] == ST_WEBHOOK:
            if event['eventType'] == 'DEVICE_EVENT':
                device = event['deviceEvent']
                emit_val = st.updateDevice(device['deviceId'], device['capability'], device['attribute'], device['value'])
                if emit_val:
                    print('emit_val: ', emit_val)
                    print('Emitting: %s: %s to room: %s' % (emit_val[0], emit_val[1], device['locationId']))
                    socketio.emit(emit_val[0],emit_val[1], room=device['locationId'])
            elif event['eventType'] == 'DEVICE_HEALTH_EVENT':
                data = event['deviceHealthEvent']
                if st.updateDeviceHealth(data['deviceId'], data['status']):
                    socketio.emit('location-data', json.dumps(st.location), room=data['locationId'])
        else:
            data = {'appId':'Not Recognized'}
            print('Event Unknown appId: %s' % content['appId'])

        return jsonify(data)

    elif (content['lifecycle'] == 'UNINSTALL'):
        print(content['lifecycle'])
        data = {'uninstallData':{}}
        return jsonify(data)

    else:
        print('Unknown Lifecycle: %s' % content['lifecycle'])
        return '',404

@app.route('/test')
def test():
    return 'OK'

@app.route('/apple-touch-icon-152x152.png')
@app.route('/apple-touch-icon-152x152-precomposed.png')
@app.route('/apple-touch-icon-120x120-precomposed.png')
@app.route('/apple-touch-icon-120x120.png')
@app.route('/apple-touch-icon-precomposed.png')
@app.route('/apple-touch-icon.png')
@app.route('/favicon.ico')
def favicon():
    print('favicon')
    return send_from_directory('/home/pi/static', 'favicon.png')

if __name__ == '__main__':
    st = SmartThings()
#    st.initialize(refresh=False) # Use this during development (after st.initialize() first) to eliminate API calls.
    st.initialize()
    socketio.run(app, debug=True, host='0.0.0.0', port=5000)
#    socketio.run(app, debug=False, host='0.0.0.0', port=5000) # Change to debug=False before deployment.

Note: After you run the server the first time and the database is fully seeded, you can comment out the st.initialize() statement towards the bottom and uncomment the st.initialize(refresh=False) statement. This will simply allow your server to start much more quickly while developing. You can reverse this once you get ready to deploy so you can be sure that the server has the current health and status of all devices at startup.

Note: It is recommended that you change your app to run with debug=False before you deploy. (socketio.run(app, debug=False, host=’0.0.0.0′, port=5000))

base.html

This is our base template for our login.html file. It uses jinja to extend this template to include our login.html code.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes">	
    <title>SmartThings Dashboard</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" />
	
    <style>
	html {
	    background-color: lightblue;
	}
	
    </style>
</head>

<body>
    <div class="container has-text-centered">
       {% block content %}
       {% endblock %}
    </div>    
</body>

</html>
login.html

This is our user login code. It uses jinja to extend the base.html file.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "base.html" %}

{% block content %}
<div class="column is-4 is-offset-4">
    <h3 class="title">Login</h3>
    <div class="box">
{% with messages = get_flashed_messages() %}
{% if messages %}
    <div class="notification is-danger">
        {{ messages[0] }}
    </div>
{% endif %}
{% endwith %}
    <form method="POST" action="/login?next={{ next_page }}">         
        <form method="POST" action="/login">
            <div class="field">
                <div class="control">
                    <input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
                </div>
            </div>

            <div class="field">
                <div class="control">
                    <input class="input is-large" type="password" name="password" placeholder="Your Password">
                </div>
            </div>
            <div class="field">
                <label class="checkbox">
                    <input type="checkbox" name="remember">
                    Remember me
                </label>
            </div>
            <button class="button is-block is-info is-large is-fullwidth">Login</button>
        </form>
    </div>
</div>
{% endblock %}
dashboard.html

This is our Smart Home Dashboard. It defines javascript classes for Room, Device, Presence, and Scene and instantiates the respective objects for each element passed to it from our server and builds the html to create the SHD display.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes">	
    <title>SmartThings Dashboard</title>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.1.3/socket.io.min.js"></script>
    <link rel="stylesheet" href="static/css/all.css" />
  	<script src="/static/autorefresh.js"></script>
    <style>
      body {
        text-align: center;
        font-size: 32px;
        background-color: teal;
        color: white;
        padding-bottom: 50px;
      }
      .overlay {
        position: fixed;
        display: none;
        height: 100%;
        width: 100%;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: rgba(169, 200, 236, 0.85);
        z-index: 2;
      }
      #header {
        margin-bottom: 10px;
      }
      #user {
        position: absolute;
        cursor: pointer;
        top: 15px;
        left: 15px;
        height: 30px;
        width: auto;
        font-size: 18px;
        display: block;
        {% if current_user.role == 'Admin' %}
        color: gold;
        {% elif current_user.role == 'User' %}
        color: white;
        {% else %}
        color: aqua;
        {% endif %}
      }
      #header-back-button {
        cursor: pointer;
        position: absolute;
        top: 15px;
        left: 15px;
        height: 30px;
        width: auto;
        font-size: 18px;
        display: none;
        --display: block;
      }
      #header-refresh-button {
        cursor: pointer;
        position: absolute;
        top: 15px;
        right: 15px;
        height: 30px;
        width: auto;
        font-size: 18px;
        display: block;
      }
      .disconnected {
        color: red;
      }
      #back-button {
        display: none;
        margin: auto;
        text-align: center;
        height: 30px;
        width: 100px;
        font-size: 18px;
      }
      #devices {
        width: 98%;
        height: auto;
        margin: auto;
        background: teal;
        padding: 5px 5px 5px 5px;
        text-align: center;
        font-size: 32px;
        box-sizing: border-box;
        overflow: auto;
      }
      .device {
        background: radial-gradient(lightblue, steelblue);
        position: relative;
        float: left;
        margin: 7px;
        height: 200px;
        width: 23%;
        color: black;
        border: 1px solid white;
        border-radius: 10px;
        box-sizing: border-box;
	  		box-shadow: 4px 4px 4px powderblue;
        overflow: hidden;
      }
      .room {
        background: radial-gradient(lightblue, steelblue);
        position: relative;
        float: left;
        margin: 5px;
        height: 100px;
        width: 32%;
        color: black;
        border: 1px solid white;
        border-radius: 10px;
        box-sizing: border-box;
	  		box-shadow: 4px 4px 4px powderblue;
        overflow: hidden;
      }
      .presence {
        background: radial-gradient(lightblue, steelblue);
        position: relative;
        float: left;
        margin: 5px;
        height: 100px;
        width: 32%;
        color: black;
        border: 1px solid white;
        border-radius: 10px;
        box-sizing: border-box;
  			box-shadow: 4px 4px 4px powderblue;
        overflow: hidden;
      }
      .presence-name {
        width: 100%;
        margin: auto;
        margin-top: 20px;
        text-align: center;
        font-size: 24px;
      }
      .scene {
        background: radial-gradient(lightblue, steelblue);
        position: relative;
        float: left;
        margin: 7px;
        height: 100px;
        width: 31%;
        color: black;
        border: 1px solid white;
        border-radius: 10px;
        box-sizing: border-box;
	  		box-shadow: 4px 4px 4px powderblue;
        overflow: hidden;        
      }
      .scene-name {
        width: 96%;
        margin: auto;
        margin-top: 20px;
        text-align: center;
        font-size: 24px;
      }
      .scene-block {
        box-shadow: 5px 5px 5px black;
      }
      .Away {
        background: radial-gradient(linen, darkgray);
      }
      .room-name {
        margin-top: 8px;
        margin-left: 5px;
        margin-right: 5px;
        font-size: 20px;
        text-align: center;
      }
      .room-main {
        position: absolute;
        top: 10px;
        margin-top: 10px;
        height: 60px;
        width: 100%;
        overflow: hidden;
        font-size: 24px;
        text-align: center;
      }
      .room-bottom {
        width: 100%;
        margin-bottom: 5px;
        font-size: 14px;
        text-align: center;
        position: absolute;
        bottom: 0px;
        left: 10px
      }
      .room-online {
        background-color: powderblue;
      }
      .room-offline {
        border: 1px solid black;
        box-shadow: 5px 5px 5px black;
      }
      .room-active {
        border: 1px solid orange;
        box-shadow: 5px 5px 5px orange;
      }
      .room-alert {
        border: 1px solid red;
        box-shadow: 5px 5px 5px red;
      }
      .room-secure-lights-off {
        background: radial-gradient(lightblue, steelblue);
      }
      .room-secure-lights-on {
        background: radial-gradient(linen 15%, palegoldenrod 35%, steelblue);
      }
      .room-unsecure-lights-off {
        background: radial-gradient(linen, crimson);
      }
      .room-unsecure-lights-on {
        background: radial-gradient(linen 15%, palegoldenrod 35%, crimson);
      }
      .device-inactive {
        box-shadow: 5px 5px 5px white;
      }
      .device-active {
        border: 1px solid orange;
        background: radial-gradient(linen, crimson);
        box-shadow: 5px 5px 5px orange;
      }
      .low-battery {
        box-shadow: 5px 5px 5px red;
      }
      .critical-battery {
        box-shadow: 5px 5px 5px red;
      }
      .device-offline {
        background: lightgrey;
        box-shadow: 5px 5px 5px black;
      }
      .device-block {
        box-shadow: 5px 5px 5px black;
      }
      .device-unsecure {
        background: radial-gradient(linen, crimson);
      }
      .device-lights {
        background: radial-gradient(linen 15%, palegoldenrod 35%, steelblue);
      }
      .device-presence-Away {
        background: radial-gradient(lightgrey, lightblue);
      }
      .device-label {
        width: 90%;
        margin: auto;
        margin-top: 8px;
        font-size: 18px;
        text-align: center;
      }
      .device-main {
        position: absolute;
        top: 60px;
        height: 60px;
        width: 100%;
        overflow: hidden;
        font-size: 24px;
        text-align: center;
      }
      .device-lower-main {
        position: absolute;
        top: 125px;
        height: 45px;
        width: 100%;
        overflow: hidden;
        font-size: 20px;
        text-align: center;
      }
      .device-bottom {
        margin-bottom: 5px;
        width: 90%;
        font-size: 14px;
        text-align: center;
        position: absolute;
        bottom: 0px;
        left: 10px
      }
      .main-icon {
        font-size: 32px;
      }
      .fa-battery-full {
        color: green;
        font-size: 16px;
      }
      .fa-battery-three-quarters {
        color: green;
        font-size: 16px;
      }
      .fa-battery-half {
        color: yellowgreen;
        font-size: 16px;
      }
      .fa-battery-quarter {
        font-size: 16px;
        color: khaki;
      }
      .fa-battery-empty {
        font-size: 16px;
        color: red;
      }
      .cooling {
        background: radial-gradient(lightyellow, lightskyblue);
      }
      .heating {
        background: radial-gradient(lightyellow, lightpink);
      }
      
      .form-background {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background-color: rgba(169, 200, 236, 0.85);
      }
      .form-popup {
        position: relative;
        top: 200px;
        max-width: 300px;
        margin: auto;
        border: 3px solid #f1f1f1;
        z-index: 9;
        color: black;
        font-size: 18px;
      }
      .form-container {
        max-width: 300px;
        padding: 10px;
        background-color: white;
      }
      .btn {
        margin: 20px;
      }
      .navbar {
        background-color: rgb(35,35,35);
        border-top: 1px solid rgb(65,65,65);
        overflow: hidden;
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
      }
      .nav-item-x {
        display: block;
        color: white;
        text-align: center;
        padding: 10x;
        min-width: 100px;
        margin: 10px 20px 15px 32px;
        text-decoration: none;
        cursor: pointer;
        font-size: 24px;
      }
      .nav-item {
        display: block;
        color: white;
        text-align: center;
        padding: 14px 16px;
        min-width: 50px;
        text-decoration: none;
        cursor: pointer;
        font-size: 24px;
      }
      .nav-item-left {
        float: left;
      }
      .nav-item-right {
        float: right;
      }
      .nav-item-x:hover {
        background-color: #ddd;
        color: black;
      }

      .active-menu {
        background-color: #04AA6D;
        color: white;
      }
      .user-menu {
          display: none;
          position: absolute;
          cursor: pointer;
          top: 45px;
          left 45px;
          padding-bottom: 10px;
          background-color: rgba(0,75,50,0.8);
          min-width: 160px;
          box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
          z-index: 1;
        }
        .user-item {
          text-decoration: none;
          color: white;
          cursor: pointer;
          font-size: 24px;
        }
        .show {
          display: block;
        }

        @media screen and (max-width: 850px) {
          #devices {
            width: 100%;
          }
          .room {
            width: 31%;
          }
          .presence {
            width: 31%;
          }
          .device {
            width: 22%;
          }
        }
      
        @media screen and (max-width: 650px) {
          #devices {
            width: 100%;
          }
            .room {
            width: 30%;
          }
          .presence {
            width: 30%;
          }
          .device {
            width: 29%;
          }
          .room-name {
            font-size: 18px;
          }
          .room-main {
            top: 15px;
            margin-top: 10px;
            font-size: 18px;
          }
          .room-bottom {
            margin-bottom: 0px;
            font-size: 14px;
          }
          .presence-name {
            margin-top: 20px;
            font-size: 18px;
          }
        }      

        @media screen and (max-width: 450px) {
          .device {
            width: 44%;
          }
          .scene {
            width: 44%;
          }
          .scene-name {
            font-size: 20px;
          }
        }

    </style>
  </head>
  <body>
    <div class="overlay" onclick="overlayOff()"></div>
    <div id="header">
      <div class="dropdown">
        <i class="fas fa-user" id="user" onclick="displayUserMenu()">  {{ current_user.name }}</i>
          <div class="user-menu">
            <a class="user-item" href="/logout">Logout</a>
          </div>
      </div>
      <i class="fas fa-angle-double-left" id="header-back-button" onclick="updateDisplay(true)"></i>
      <span id="header-label" class="disconnected"></span>
      {% if current_user.role != 'Guest' %}
      <span><i class="fas fa-sync" id="header-refresh-button" onclick="refresh()"></i></span>
      {% endif %}
    </div>
    <div id="devices" class="display-rooms"></div>
    <p><button id="back-button" type="button" onclick="updateDisplay(true)">Back</button></p>      
    <div class="form-background" id="form-background">
      <div class="form-popup" id="myForm"></div>
    </div>
    <div class="navbar" id="navbar">
      <a id="menu-home" class="nav-item nav-item-left" onclick="updateDisplay(true)"><i class="fas fa-home"></i></a>
      <a id="menu-scenes" class="nav-item nav-item-left" onclick="displayScenes()">Scenes</a>
      {% if current_user.role == 'Admin' %}
      <a href="/admin" target="_blank"><i class="nav-item nav-item-right fas fa-cog"></i></a>
      {% endif %}
    </div>
    
  <script>
  // Send a system ping every 15 minutes
  setInterval(pingBack, (1000*60*15));
  
  function pingBack() {
    socket.emit('pingBack');
  }
  
  var locationData;
	var protocol = window.location.protocol;
  const DOW_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

  function displayUserMenu() {
    document.querySelector(".user-menu").classList.toggle("show");
  }
  
  function bodyClick() {
  }
  
  document.addEventListener("click", function(event) {
    if (!event.target.matches("#user")) {
      document.querySelector(".user-menu").classList.remove("show");
    }
  });
  
	function getTimeDisplay(dt) {
		var time_str = "";
		var hrs = "";
		var mins = "";
		var ap = "AM";
		var val = 0;
		
		val = dt.getHours();
		if (val >=12) {
			ap = "PM";
			if (val > 12) {
				val -= 12;
			}
		}
		else if (val == 0) {
			val = 12;
		}
		hrs = val.toString();
		
		val = dt.getMinutes();
		mins = ("0" + val).slice(-2);
		
		time_str = hrs + ":" + mins + " " + ap;
		return time_str;
	}
  
  var overlay = document.querySelector(".overlay");
  function overlayOff() {
    overlay.style.display = "none";
  }

  var socket = io.connect(protocol + '//' + document.domain + ':' + location.port, {
		reconnection: true,
		reconnectionDelay: 1000,
		reconnectionDelayMax: 10000,
		reconnectionAttempts: 99999
	});

	socket.on('conn', function(msg) {
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		console.log(dtDisp + " - " + data.status);
    headerLabel.classList.remove("disconnected");

		const d = new Date();
		d.setTime(d.getTime() + (1*24*60*60*1000));
		let expires = "expires=" + d.toUTCString();
    document.cookie = "username" + "=" + "jeff" + ";" + expires + ";path=/;host;secure;";
	});
  
  socket.on('disconnect', function(msg) {
    console.log("Disconnected!");
    headerLabel.classList.add("disconnected");
  });

  socket.on('pingRcv', function() {
    console.log("pingRcv");
  });
  
	socket.on('location_data', function(msg) {
		console.log("location-data");
    
    // If nothing is passed to location-data, user is no longer authorized to view dashboard.  Blank it out!
    if (!msg) {
        socket.emit('disconn');
        socket = null;
        document.body.innerHTML = "<div>Not Authorized!<br />Contact System Administrator!</div>";
    } else {
      var dt = new Date();
      var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
      locationData = JSON.parse(msg);
//      console.log(JSON.stringify(locationData, null, 2));
      buildDisplay();
      overlay.style.display = "none";
    }
	});

	socket.on('presence_chg', function(msg) {
		console.log("presence_chg: " + msg);
    data = JSON.parse(msg);
		presenceChange(data);
    overlay.style.display = "none";
	});

	socket.on('device_chg', function(msg) {
		console.log("device_chg: " + msg);
		data = JSON.parse(msg);
    deviceChange(data);
    overlay.style.display = "none";
	});

  function refresh() {
    overlay.style.display = "block";
    socket.emit("refresh");
  }
    
  Array.prototype.sortOn = function(key){
      this.sort(function(a, b){
          if(a[key] < b[key]){
              return -1;
          }else if(a[key] > b[key]){
              return 1;
          }
          return 0;
      });
  }

class Device {
  constructor(device) {
    this.device = device;
    this.resetDevice();
    this.html = this.buildHtml();
  }
  log() {
    console.log(this.device);
  }
  resetDevice() {
    this.offline = false;
    this.block = false;
    this.temperature = "";
    this.humidity = "";
    this.battery = "";
    this.unsecure = "";
    this.active = "";
    this.lights = "";  
    this.presence = false;
    this.presenceState = "";
    this.icon = "";
  }
  getStatus() {
    var status = {"offline": this.offline, "temperature": this.temperature, "humidity": this.humidity, "battery": this.battery,
                  "unsecure": this.unsecure, "active": this.active, "lights": this.lights};
    return status;
  }
  getHtml() {
    return this.html;
  }
  getDevice(deviceId) {
    if (deviceId === this.device.deviceId) {
      return this;
    }
    return null;
  }
  update(deviceData) {
    this.device.capabilities.forEach(capability => {
      if (capability.id === deviceData.capability) {
        capability.state = deviceData.value;
        this.html = this.buildHtml();
        return true;
      }
      return false;
    });
  }
  buildHtml() {
    var elements = [];
    var topSection = `<div class="device-label">${this.device.label}</div>`;
    var mainSection = "";
    var lowerMainSection = "";
    var bottomSection = "";
    var temp = null;
    var humid = null;
    var thermostateMode = "";
    var thermostatOperatingState = "";
    var coolingSetpoint = "";
    var heatingSetpoint = "";
    var fanMode = "";
    var shadow =  ""; 
    this.icon = this.device.icon;
    this.offline = this.device.health == "OFFLINE" ? true : false;
    this.block = false;
    {% if current_user.role == "Guest" %}
    if (!this.device.guest_access) {
      console.log("Guest Access: " + this.device.label);
      this.block = true;
    }
    {% endif %}
    var onclick = this.block ? "" : "onclick='deviceClick(this)'";
    this.device.capabilities.sortOn("seq");
    this.device.capabilities.forEach(capability => {
      var capData = "";
      var capHtml = "";
      switch (capability.id) {
        case "switch":
          var iconClass;
          if (capability.state == "on" && !this.offline) {
            this.lights = true;
            iconClass = this.icon ? this.icon : "far fa-lightbulb";
          } else {
            iconClass = this.icon ? this.icon : "far fa-lightbulb";
          }
          iconClass += " main-icon"
          var icon = `<i class="${iconClass}"></i>`;
          capHtml = `<div id="${this.device.deviceId}" class="device-main ${capability.id} ${capability.state}" ${onclick}>${icon}</div>`;
          elements.push({"type": "switch", "html": capHtml});
          break;
 
        case "lock":
          var iconClass;
          if (capability.state == "unlocked" && !this.offline) {
            this.unsecure = true;
            iconClass = "fas fa-lock-open";
          } else {
            iconClass = "fas fa-lock";
          }
          iconClass += " main-icon"
          var icon = `<i class="${iconClass}"></i>`;
          capHtml = `<div id="${this.device.deviceId}" class="device-main ${capability.id} ${capability.state}" ${onclick}>${icon}</div>`;
          elements.push({"type": "lock", "html": capHtml});
          break;

        case "doorControl":
          var iconClass;
          if (capability.state == "open" && !this.offline) {
            this.unsecure = true;
            iconClass = "fas fa-door-open";
          } else {
            iconClass = "fas fa-door-closed";
          }
          iconClass += " main-icon"
          var icon = `<i class="${iconClass}"></i>`;
          capHtml = `<div id="${this.device.deviceId}" class="device-main ${capability.id} ${capability.state}" ${onclick}>${icon}</div>`;
          elements.push({"type": "doorControl", "html": capHtml});
          break;
        
        case "contactSensor":
        var iconClass;
          if (capability.state == "open" && !this.offline) {
            this.unsecure = true;
            iconClass = "fas fa-door-open";
          } else {
            iconClass = "fas fa-door-closed";
          }
          iconClass += " main-icon"
          var icon = `<i class="${iconClass}"></i>`;
          capHtml = `<div class="device-main ${capability.id} ${capability.state}">${icon}</div>`;
          elements.push({"type": "contactSensor", "html": capHtml});
          break;

        case "motionSensor":
        var iconClass;
          if (capability.state == "active" && !this.offline) {
            this.active = true;
            iconClass = "fas fa-running";
          } else {
            iconClass = "fas fa-running";
          }
          iconClass += " main-icon"
          var icon = `<i class="${iconClass}"></i>`;
          capHtml = `<div class="device-main ${capability.id} ${capability.state}">${icon}<br />${capability.state[0].toUpperCase() + capability.state.substring(1)}</div>`;
          elements.push({"type": "motionSensor", "html": capHtml});
          break;
      
        case "presenceSensor":
          var pres = capability.state == "present" ? "Home" : "Away";
          this.presence = true;
          this.presenceState = pres;
          capHtml = `<div class="device-main ${capability.id} ${capability.state}" ${onclick}>${pres}</div>`;
          elements.push({"type": "presenceSensor", "html": capHtml});
          break;
      
        case "thermostatMode":
          thermostateMode = capability.state;
          capHtml = "";
          elements.push({"type": "thermostatMode", "html": capHtml});
          break;

        case "thermostatOperatingState":
          thermostatOperatingState = capability.state;
          if (["heating","cooling"].includes(capability.state) && !this.offline) {
            this.active = true;
          }
          break;

        case "thermostatCoolingSetpoint":
          coolingSetpoint = capability.state;
          break;

        case "thermostatHeatingSetpoint":
          heatingSetpoint = capability.state;
          break;

        case "thermostatFanMode":
          fanMode = capability.state;
          break;

        case "temperatureMeasurement":
          temp = capability.state;
          this.temperature = Math.round(temp);
          break;

        case "relativeHumidityMeasurement":
          humid = capability.state;
          this.humidity = humid;
          break;

        case "battery":
          var iconClass;
          var battery_level = parseInt(capability.state);
          if (battery_level >= 80) {
            iconClass = "fas fa-battery-full";
          } else if (battery_level >= 60) {
            iconClass = "fas fa-battery-three-quarters";
          } else if (battery_level >= 40) {
            iconClass = "fas fa-battery-half";
          } else if (battery_level >= 30) {
            iconClass = "fas fa-battery-quarter";
          } else {
            iconClass = "fas fa-battery-empty";
          }
          var icon = `<i class="${iconClass}"></i>`;

          bottomSection = `<div class="device-bottom ${capability.id} ${capability.state}">${icon} ${capability.state}%</div>`;
          if (!this.offline) {
            this.battery = battery_level;
          }
          break;

        case "switchLevel":
          mainSection += `<br /><div id="level-${this.device.deviceId}" class="device-lower-main switch-level">${capability.state}%</div>`;
          bottomSection = `<div class="device-bottom ${capability.id} ${capability.state} switch-level"><input class="slider" 
                           id="${this.device.deviceId}-${capability.id}" data-deviceId="${this.device.deviceId}" onchange="sliderChange(this)" 
                           type="range" min="1" max="100" value="${capability.state}" 
                           oninput="setSliderValue(this)"></div>`;
        break;

        default:
          console.log("Unknown capability: " + capability.id);
          break;
      }
    });
    elements.forEach(element => {
      if (element.type == "lock") {
        mainSection = element.html;
      } else if (element.type == "doorControl") {
          mainSection = element.html;
      } else if (element.type == "contactSensor") {
        if (!mainSection) {
          mainSection = element.html;
        } else {
          lowerMainSection = element.html;
        }
      } else if (element.type == "motionSensor") {
        if (!mainSection) {
          mainSection = element.html;
        } else {
          lowerMainSection = element.html;
        }
      } else if (element.type == "switch") {
        if (!mainSection) {
          mainSection = element.html;
        } else {
          lowerMainSection = element.html;
        }
      } else if (element.type == "thermostatMode") {
        if (!mainSection) {
          var setpoint = thermostateMode == "cool" ? ": " + coolingSetpoint + "&#176" : (thermostateMode == "heat" ? ": " + heatingSetpoint + "&#176" : "");
          mainSection = `<div id="${this.device.deviceId}" class="device-main thermostatMode ${thermostateMode}" ${onclick}>${thermostateMode[0].toUpperCase() + thermostateMode.substring(1)} ${setpoint}<br />`;
          mainSection += `${thermostatOperatingState[0].toUpperCase() + thermostatOperatingState.substring(1)}</div>`;
        } 
      } else if (element.type == "presenceSensor") {
        if (!mainSection) {
          mainSection = element.html;
        } else {
          lowerMainSection = element.html;
        }
      }        
    });
    if (!mainSection) {
        if (temp && humid) {
          mainSection = `<div class="device-main temperatureMeasurement humidityMeasurement">${this.temperature}&#176<br />${humid}%</div>`;
        } else if (temp) {
          mainSection = `<div class="device-main temperatureMeasurement">${this.temperature}&#176</div>`;
        }else if (humid) {
          mainSection = `<div class="device-main humidityMeasurement">${humid}%</div>`;
        }
    } else if (!lowerMainSection) {
      if (temp && humid) {
          lowerMainSection = `<div class="device-lower-main temperatureMeasurement humidityMeasurement">${this.temperature}&#176 - ${humid}%</div>`;
        } else if (temp) {
          lowerMainSection = `<div class="device-lower-main temperatureMeasurement">${this.temperature}&#176</div>`;
        }else if (humid){
          lowerMainSection = `<div class="device-lower-main humidityMeasurement">${humid}%</div>`;
        }
    }        

    var state = this.offline ? "device-offline" : 
                (this.unsecure ? "device-unsecure" : (this.lights ? "device-lights" : 
                (this.active ? "device-active" : (this.presence ? "device-presence-" + this.presenceState : "device-inactive"))));
                
    var blocked = this.block ? "device-block" : "";

    var html = `<div id="${this.device.deviceId}" class="device ${state} ${thermostatOperatingState} ${blocked}">`;
    html += topSection + mainSection + lowerMainSection + bottomSection;
    html += `</div>`;
    return html;
  }
  hasCapability(capability) {
    for (var x = 0; x < this.device.capabilities.length; x++) {
      if (this.device.capabilities[x].id === capability) {
        return this.device.capabilities[x];
      }
    }
    return null;
  }
  
  getState(capability) {
    for (var x = 0; x < this.device.capabilities.length; x++) {
      if (this.device.capabilities[x].id === capability) {
        return this.device.capabilities[x];
      }
    }
    return null;
  }

  deviceClicked(deviceId) {
    if (this.device.deviceId === deviceId) {
      var capability = null;
      if ( capability = this.hasCapability("doorControl")) {
        updateDevice(deviceId, capability.id, capability.state == 'open' ? 'close' : 'open');
        return true;
      } else if (capability = this.hasCapability("lock")) {
        updateDevice(deviceId, capability.id, capability.state == 'locked' ? 'unlock' : 'lock');
        return true;
      } else if (capability = this.hasCapability("switch")) {
        updateDevice(deviceId, capability.id, capability.state == 'on' ? 'off' : 'on');
        return true;
      } else if (capability = this.hasCapability("thermostatMode")) {
        var heat = this.hasCapability("thermostatHeatingSetpoint");
        var cool = this.hasCapability("thermostatCoolingSetpoint");
          var popup = `<form name="thermostat-form" action="" class="form-container" onsubmit="return saveThermostatSettings()">
            <h3 style="font-size:20px;text-align:center;margin-top: 0px;">${this.device.label}</h3>

            <input type="text" name="device-id" value="${deviceId}" hidden>
            <label for="modes"><b>Mode: </b></label>
            <select name="modes" id="modes">
              <option value="off" ${capability.state == "off" ? "selected" : ""}>Off</option>
              <option value="cool" ${capability.state == "cool" ? "selected" : ""}>Cool</option>
              <option value="heat" ${capability.state == "heat" ? "selected" : ""}>Heat</option>
            </select>
            <br />
            <br />
            <label for="heat-setpoint"><b>Heating Temperature:</b></label>
            <input type="number" name="heat-setpoint" required value="${heat.state}" min="50" max="80">
            <br />
            <br />
            <label for="cool-setpoint"><b>Cooling Temperature:</b></label>
            <input type="number" name="cool-setpoint" required value="${cool.state}" min="50" max="80">
            <br />
            <br /> 
            <button type="submit" class="btn">Save</button>
            <button type="button" class="btn cancel" onclick="closeForm()">Cancel</button>
          </form>`;
          document.getElementById("myForm").innerHTML = popup;
          document.getElementById("form-background").style.display = "block";
        return true;
      } else {
        console.log("Nothing to do!");
        return true;
      }
    }
    return false;
  }
}

function setSliderValue(obj) {
  var deviceId = obj.getAttribute("data-deviceId");
  var parent = document.querySelector("#level-" + deviceId);
  parent.innerText = obj.value + "%";
  parent.style.color = "red";
}

function sliderChange(obj) {
  var slider = document.getElementById(obj.getAttribute("id"));
  var level = document.getElementById("level-" + obj.getAttribute("data-deviceId"));
  var deviceId = obj.getAttribute("data-deviceId");
  level.innerHTML = slider.value + "%";
  updateDevice(deviceId, "switchLevel", slider.value);
};

function updateDevice(deviceId, capabilityId, state) {
  var dJson = {"deviceId": deviceId, "capability": capabilityId, "state": state};
//  console.log("dJson: " + dJson);
  socket.emit('update-device', dJson);
  return false;
}

function saveThermostatSettings() {
  let tForm = document.forms["thermostat-form"];
  let deviceId = tForm["device-id"].value;
  let mode = tForm["modes"].value;
  let heatingSetpoint = tForm["heat-setpoint"].value;
  let coolingSetpoint = tForm["cool-setpoint"].value;
  
  var tJson = {"deviceId": deviceId, "commands": [
    {"capability": "thermostatMode", "value": mode}, 
    {"capability": "thermostatHeatingSetpoint", "value": heatingSetpoint}, 
    {"capability": "thermostatCoolingSetpoint", "value":  coolingSetpoint}
    ]};

//  console.log("tJson: " + JSON.stringify(tJson));

  socket.emit('update-thermostat', tJson);

  closeForm();
  return false;
}

function closeForm() {
  document.getElementById("form-background").style.display = "none";
}

class Room {
  constructor(room) {
    this.room = room;
    this.resetRoom();
    this.html = this.buildHtml();
  }
  resetRoom() {
    this.offline = "room-online";
    this.temperature = "";
    this.humidity = "";
    this.battery = "";
    this.unsecure = "room-secure";
    this.active = "";
    this.lights = "room-lights-off";
    this.devices = [];
  }
  getName() {
    return this.room.name;
  }
  getID() {
    return this.room.roomId;
  }
  deviceClick(deviceId) {
    for (var x = 0; x < this.devices.length; x++) {
      if (this.devices[x].deviceClicked(deviceId)) {
        return true;
      }
    }
    return false;
  }
  updateDevice(deviceData) {
    var updated = false;
    this.devices.forEach(device => {
      if (device.getDevice(deviceData.deviceId)) {
        device.update(deviceData);
        this.resetRoom();
        this.html = this.buildHtml();
        updated = true;
      }
    });
    return updated;
  }
  getHtml() {
    if (!this.devices.length) {
      return "";
    }
    return this.html;
  }
  buildHtml() {
    this.room.devices.sortOn("seq");
    this.room.devices.forEach(device => {
      var dev = new Device(device);
      this.devices.push(dev);
      var status = dev.getStatus();
      if (status.offline) {
        this.offline = "room-offline";
      }
      if (status.temperature) {
        this.temperature = status.temperature;
      }
      if (status.humidity) {
        this.humidity = status.humidity;
      }
      if (status.battery) {
        if (!this.battery) {
          this.battery = status.battery;
        } else if (status.battery < this.battery) {
          this.battery = status.battery;
        }
      }
      if (status.unsecure) {
        this.unsecure = "room-unsecure";
      }
      if (status.active) {
        this.active = status.active;
      }
      if (status.lights) {
        this.lights = "room-lights-on";
      }
    });

    var isUnsecure = this.unsecure == "room-unsecure" ? true : false;
    var isLights = this.lights == "room-lights-on" ? true : false;
    var secureClass = "room-secure-lights-off";
    if (isUnsecure || isLights) {
      if (isUnsecure && !isLights) {
        secureClass = "room-unsecure-lights-off";
      } else if (!isUnsecure && isLights) {
        secureClass = "room-secure-lights-on";
      } else {
        secureClass = "room-unsecure-lights-on"
      }
    }

    var tempData = "";
    if (this.temperature && this.humidity) {
      tempData = `${this.temperature}&#176 - ${this.humidity}%`;
    } else if (this.temperature) {
      tempData = `${this.temperature}&#176`;
    }else if (this.humidity) {
      tempData = `${this.humidity}%`;
    }
    var mainHtml = `<div class="room-main">${tempData}</div>`;

    var bottomSection = "";
    var iconClass = "";
    if (this.battery) {
      var battery_level = parseInt(this.battery);
      if (battery_level >= 80) {
        iconClass = "fas fa-battery-full";
      } else if (battery_level >= 60) {
        iconClass = "fas fa-battery-three-quarters";
      } else if (battery_level >= 40) {
        iconClass = "fas fa-battery-half";
      } else if (battery_level >= 30) {
        iconClass = "fas fa-battery-quarter";
      } else {
        iconClass = "fas fa-battery-empty";
      }
      var icon = `<i class="${iconClass}"></i>`;

      bottomSection = `<div class="room-bottom">${icon} ${battery_level}%</div>`;
    }

    var stateClass = `${this.offline} ${this.active ? "room-active" : ""} ${iconClass.includes("battery-empty") ? "room-alert" : ""} ${secureClass}`;

    var html = `<div id="${this.room.roomId}" class="room ${stateClass}" onclick="roomClick(this)">`;
    html += `<div class="room-name">${this.room.name}</div>`;
    html += `<div class="room-main">${mainHtml}</div>`;
    html += bottomSection;
    html += `</div>`;
    return html;
  }
  displayDevices() {
    var html = "";
    this.devices.forEach(device => {
      html += device.getHtml();
    });
    return html;
  }
}

class Presence {
  constructor(presence) {
    this.presence = presence;
    this.state = null;
    this.html = this.buildHtml();
  }
  getHtml() {
    return this.html;
  }
  buildHtml() {
    var presenceSensor = null;
    this.presence.capabilities.forEach(capability => {
      if (capability.id == "presenceSensor") {
        presenceSensor = capability;
      }
    });
    this.state = presenceSensor.state == "present" ? "Home" : "Away";
    var html = `<div id="${this.presence.deviceId}" class="presence ${this.state}">`;
    html += `<div class="presence-name">${this.presence.label}<br />${this.state}</div>`;
    html += `</div>`;
    return html;
  }
  updateDevice(presenceData) {
    var status = false;
    if (this.presence.deviceId === presenceData.deviceId) {
      this.presence.capabilities.forEach(capability => {
        if (capability.id === presenceData.capability) {
          capability.state = presenceData.value;
          this.html = this.buildHtml();
          status = true;
        }
      });
    }
    return status;
  }  
}

class Scene {
  constructor(scene) {
    this.scene = scene;
    this.block = false;
    {% if current_user.role == 'Guest' %}
    if (!this.scene.guest_access) {
      this.block = true;
    }
    {% endif %}
    this.html = this.buildHtml();
  }
  getHtml() {
    return this.html;
  }
  buildHtml() {
    var blocked = this.block ? "scene-block" : "";
    var onclick = this.block ? "" : "onclick='sceneClick(this)'";
    var html = `<div id="${this.scene.scene_id}" data-scene="${this.scene.name}" class="scene ${blocked}" ${onclick}>`;
    html += `<div class="scene-name">${this.scene.name}</div>`;
    html += "</div>";
    return html;
  }
}

function sceneClick(scene) {
  var sceneId = scene.getAttribute("ID");
  var sceneName = scene.getAttribute("data-scene");
  
  if (confirm("Run Scene?\n" + sceneName)) {
    socket.emit('run-scene', {"scene_id": sceneId})
  }
}

function roomClick(room) {
  var roomId = room.getAttribute("ID");

  for (var x = 0; x < rooms.length; x++) {
    if (rooms[x].getID() == roomId) {
      roomIdx = x;
      displayArea.classList.add("display-devices");
      displayArea.classList.remove("display-rooms");
      displayArea.classList.remove("display-scenes");
      userLabel.style.display = "none";
      var hbb = getComputedStyle(headerBackButton);
      var hbbDisplay = hbb.getPropertyValue("--display");
//      console.log("--display: " + hbbDisplay);
      headerBackButton.style.display = hbbDisplay; //"block";
      backButton.style.display = "block";
      headerLabel.innerHTML = rooms[x].room.name;
      displayArea.innerHTML = rooms[x].displayDevices();
      break;
    }
  }
}

function deviceClick(device) {
  var deviceId = device.getAttribute("ID");
  for (var x = 0; x < rooms.length; x++) {
    if (rooms[x].deviceClick(deviceId)) {
      break;
    }
  }
}

var roomIdx = 0;
var rooms = [];
var presence = [];
var scenes = [];
var userLabel = document.getElementById("user");
var headerBackButton = document.getElementById("header-back-button");
var hbb = getComputedStyle(headerBackButton);
var hbbDisplay = hbb.getPropertyValue("--display");
var headerLabel = document.getElementById("header-label");
var displayArea = document.getElementById("devices");
var backButton = document.getElementById("back-button");

function buildDisplay() {
  rooms = [];
  presence = [];
  scenes = [];

  locationData.rooms.sortOn("seq");
  locationData.rooms.forEach(roomItem => {
    {% if current_user.role == 'Guest' %}
    if (roomItem.guest_access == 1) {
      room = new Room(roomItem);
      rooms.push(room);
    }
    {% else %}
    room = new Room(roomItem);
    rooms.push(room);
    {% endif %}
  });

  if (locationData.presence) {
    locationData.presence.sortOn("seq");
    locationData.presence.forEach(presItem => {
      pres = new Presence(presItem);
      presence.push(pres);
    });
  }

  if (locationData.scenes) {
    locationData.scenes.sortOn("seq");
    locationData.scenes.forEach(sceneItem => {
      scene = new Scene(sceneItem);
      scenes.push(scene);
    });
  }
  updateDisplay();
}

function updateDisplay(roomDisplay=false) {
  if (roomDisplay) {
    displayArea.classList.add("display-rooms");
    displayArea.classList.remove("display-devices");
    displayArea.classList.remove("display-scenes");
  }
  if (displayArea.classList.contains("display-scenes")) {
    document.querySelector("#menu-scenes").classList.add("active-menu");
    document.querySelector("#menu-home").classList.remove("active-menu");
  } else {
    document.querySelector("#menu-scenes").classList.remove("active-menu");
    document.querySelector("#menu-home").classList.add("active-menu");
  }
  if (displayArea.classList.contains("display-rooms")) {
    userLabel.style.display = "block";
    headerBackButton.style.display = "none";
    headerLabel.innerHTML = locationData.location.name;
    backButton.style.display = "none";
    displayArea.innerHTML = "";
    rooms.forEach(room => {
      displayArea.innerHTML += room.getHtml();
    });

    presence.forEach(pres => {
      displayArea.innerHTML += pres.getHtml();
    });
    
  } else if (displayArea.classList.contains("display-devices")) {
    userLabel.style.display = "none";
    var hbb = getComputedStyle(headerBackButton);
    var hbbDisplay = hbb.getPropertyValue("--display");
    headerBackButton.style.display = hbbDisplay; //"block";
    headerLabel.innerHTML = rooms[roomIdx].room.name;
    backButton.style.display = "block";
    displayArea.innerHTML = rooms[roomIdx].displayDevices();
  } else if (displayArea.classList.contains("display-scenes")) {
    displayArea.innerHTML = "";
    scenes.forEach(scene => {
      displayArea.innerHTML += scene.getHtml(); 
    });  
  }
}

function deviceChange(deviceData) {
  rooms.forEach(room => {
    if (room.updateDevice(deviceData)) {
      updateDisplay();
    }
  });
}

function presenceChange(presenceData) {
  presence.forEach(pres => {
    if (pres.updateDevice(presenceData)) {
      updateDisplay();
    }
  });
}

function displayScenes() {
    displayArea.classList.add("display-scenes");
    displayArea.classList.remove("display-devices");
    displayArea.classList.remove("display-rooms");
    updateDisplay();
}

</script>
  </body>
</html> 
admin_base.html

This is our base admin html file. It contains our admin menu that will be shared across all admin pages.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes">	
    <title>SmartThings Admin Panel</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" />
    <link rel="stylesheet" href="static/css/all.css" />
	
    <style>
	html {
	    background-color: lightblue;
	    font-size: 24px;
	}
	/* Add a black background color to the top navigation */
	.topnav {
	    background-color: #333;
	    overflow: hidden;
	    margin-bottom: 30px;
	}

	/* Style the links inside the navigation bar */
	.topnav a {
	    float: left;
	    display: block;
	    color: #f2f2f2;
	    text-align: center;
	    padding: 14px 16px;
	    text-decoration: none;
	}

	/* Change the color of links on hover */
	.topnav a:hover {
	    background-color: #ddd;
	    color: black;
	}

	/* Add an active class to highlight the current page */
	.topnav a.active, .topnav .dropbtn.active {
	    background-color: #04AA6D;
	    color: white;
	}

	.hamburger-menu {
	    float: right;
	    display: none;
	}
	.full-menu {
	    display: block;
	}
	.menu-item {
	    min-width: 120px;
	}

	.topnav.responsive .full-menu {
	    float: none;
	    display: block;
	    text-align: left;
	}

	/* The dropdown container */
	.dropdown {
	    float: left;
	}

	/* Dropdown button */
	.dropdown .dropbtn {
	    font-size: 24px;
	    border: none;
	    outline: none;
	    color: white;
	    text-align: center;
	    padding: 16px 16px;
	    background-color: inherit;
	    font-family: inherit; /* Important for vertical align on mobile phones */
	}

	/* Dropdown content (hidden by default) */
	.dropdown-content {
	    display: none;
	    position: fixed;
	    top: 60px;
	    left: 120px;
	    background-color: #f9f9f9;
	    min-width: 160px;
	    min-height: 100px;
	    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
	    z-index: 1;
	}

	/* Links inside the dropdown */
	.dropdown-content a {
	    float: none;
	    color: black;
	    padding: 12px 16px;
	    text-decoration: none;
	    display: block;
	    text-align: left;
	}

	/* Add a grey background color to dropdown links on hover */
	.dropbtn:hover, .dropdown-content a:hover {
	    background-color: #ddd;
	    color: black;
	}

	/* Show the dropdown menu on hover */
	.dropdown:hover .dropdown-content {
	    display: block;
	}


	.current-user {
	    float: right;
	    color: white;
	    padding: 14px 16px;
	}

	/* Room Config Styles */
	.location-data {
	    width: 90%;
	    margin: 30px auto;
	}
	.user-table, .presence-table, .scene-table, .logging-table, .failed-table, .log-table {
	    background-color: white;
	    margin: 30px auto;
	}

	table {
	    width: 90%;
	    margin-top: 30px;
	}

	table, th, td {
	    border: 1px solid black;
	    border-collapse: collapse;
	}

	th, td {
	    padding: 3px;
	}

	select {
	    width: 100%;
	    font-size: 24px;
	}

	input[type="checkbox"] {
	    width: 20px;
	    height: 20px;
	}
	input[type="number"] {
	    text-align: right;
	}
	input {
	    font-size: 20px;
	    width: 100%;
	}

	.room, .room-header {
	    background-color: white;
	}

	.device, .device-header {
	    background-color: khaki;
	    display: none;
	}

	.capability, .capability-header {
	    background-color: beige;
	    display: none;
	}

	.right-align {
	    text-align: right;
	}

	.not-visible {
	    background-color: lightgray;
	}

	.curr-user {
	    background-color: yellow;
	}

	.collapse {
	    display: none;
	}

	.save-container {
	    width: 100%;
	    text-align: center;
	    margin: auto;
	}

	.save-button, .add-button {
	    margin-left: auto;
	    width: 100px;
	    height: 50px;
	    font-size: 24px;
	}

	.unchanged {
	    color: blue;
	    background-color: white;
	}

	.changed {
	    color: black;
	    background-color: red;
	}

	.form-background {
	    display: none;
	    position: fixed;
	    top: 0;
	    left: 0;
	    bottom: 0;
	    right: 0;
	    background-color: rgba(169, 200, 236, 0.85);
	}
	#add-user-form {
	    position: relative;
	    top: 100px;
	    max-width: 500px;
	    margin: auto;
	    padding: 20px;
	    border: 3px solid #f1f1f1;
	    z-index: 9;
	    color: black;
	    background-color: white;
	    font-size: 18px;
	    overflow: auto;
	}

	@media screen and (max-width: 500px) {
	    body {
		font-size: 18px;
	    }
	    select {
		font-size: 18px;
	    }
	    .topnav .hamburger-menu {
		display: block;
	    }
	    .topnav .full-menu {
		display: none;
	    }
	    .topnav a {
		float: none;
		padding: 10px 12px;
		text-align: left;
	    }
	    .dropdown {
		float: none;
	    }
	    .dropdown .dropbtn {
		font-size: 18px;
		padding: 12px 12px;
	    }
	    .dropdown-content {
		top: 47px;
		left: 85px;
	    }
	    .dropdown-content a {
		padding: 3px 5px;
	    }
	    .current-user {
		padding: 10px 12px;
	    }
	}

    </style>
</head>

<body>
    <div class="topnav" id="topnav">
	<div class="current-user">
	    <i class="fas fa-user">   {{ current_user.name }}</i>
	</div>
	<div class="hamburger-menu">
	    <a href="javascript:void(0);" class="icon-menu" onclick="myFunction()">
		<i class="fa fa-bars"></i>
	    </a>
	</div>	
	<a class="menu-item" id="home-menu" href="/admin">Home</a>
	    <div class="full-menu">
		<div class="dropdown">
		    <button id="users-menu" class="dropbtn">Users
		      <i class="fa fa-caret-down"></i>
		    </button>
		    <div class="dropdown-content">
		      <a href="/admin-users">Maintain Users</a>
		      <a href="/admin-logging">Log Settings</a>
		      <a href="/admin-failed-logins">Failed Logins</a>
		      <a href="/admin-view-logs">View Logs</a>
		    </div>
		</div>
		<a class="menu-item" id="rooms-menu" href="/config-rooms">Rooms</a>
		<a class="menu-item" id="presence-menu" href="/config-presence">Presence</a>
		<a class="menu-item" id="scenes-menu" href="/config-scenes">Scenes</a>
	    </div>
	</div>
    </div>
    <div class="container has-text-centered">
       {% block content %}
       {% endblock %}
    </div>    
    
    <script>
	function myFunction() {
	    var x = document.getElementById("topnav");
	    if (x.className === "topnav") {
		x.className += " responsive";
	    } else {
		x.className = "topnav";
	    }
	}    
    </script>
</body>

</html>
admin_home.html

This is our Admin home page. It provides a series of buttons that can be used to resync our database with SmartThings if needed.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
	<h1>Admin Home Page</h1>
	<p>
		These pages will allow you to access Admin-Only views and features.<br />
		Use the menus above to navigate to the desired feature.
	</p>
	<p>
		You can use the links below to refresh the database from SmartThings.<br />
		This can be helpful if you add/remove/rename something or have to resync data for some reason.<br />
	</p>
	<div class="columns is-three-quarters-mobile">
		<div class="column">
			<button type="button" class="button is-info is-medium" id="btnRefreshScenes" onclick="refreshScenes()">Refresh Scenes</button>
		</div>
		<div class="column">
			Use if you add/delete/rename a scene
		</div>
	</div>
	<div class="columns is-three-quarters-mobile">
		<div class="column">
			<button type="button" class="button is-info is-medium" id="btnRefreshStatus" onclick="refreshDeviceStatus()">Refresh Device Status</button>
		</div>
		<div class="column">
			Refresh all devices status
		</div>
	</div>
	<div class="columns is-three-quarters-mobile">
		<div class="column">
			<button type="button" class="button is-info is-medium" id="btnRefreshHealth" onclick="refreshDeviceHealth()">Refresh Device Health</button>
		</div>
		<div class="column">
			Refresh all devices health
		</div>
	</div>
	<div class="columns is-three-quarters-mobile">
		<div class="column">
			<button type="button" class="button is-info is-medium" id="btnRefreshFoundation" onclick="refreshFoundation()">Refresh Foundation Data</button>
		</div>
		<div class="column">
			Refresh Foundation Data (App, Location, Rooms and Devices)
		</div>
	</div>

<script>
	document.querySelector("#home-menu").classList.add("active");
	function refreshScenes() {
		document.querySelector("#btnRefreshScenes").classList.add("is-loading");
		var furl = "/admin-refresh-scenes";
		var xhttp=new XMLHttpRequest();
		xhttp.onreadystatechange = function() {
			if (this.readyState == 4 && this.status == 200) {
				if (this.response != "OK") {
				alert("Update Failed! Please try again.");
				} else {
					document.querySelector("#btnRefreshScenes").classList.remove("is-loading");
				}
			}
		};
		xhttp.open("POST", furl);
		xhttp.send();		
	}
	
	function refreshDeviceStatus() {
		document.querySelector("#btnRefreshStatus").classList.add("is-loading");
		var furl = "/admin-refresh-device-status";
		var xhttp=new XMLHttpRequest();
		xhttp.onreadystatechange = function() {
			if (this.readyState == 4 && this.status == 200) {
				if (this.response != "OK") {
					alert("Update Failed! Please try again.");
				} else {
					document.querySelector("#btnRefreshStatus").classList.remove("is-loading");
				}
			}
		};
		xhttp.open("POST", furl);
		xhttp.send();		
	}
	
	function refreshDeviceHealth() {
		document.querySelector("#btnRefreshHealth").classList.add("is-loading");
		var furl = "/admin-refresh-device-health";
		var xhttp=new XMLHttpRequest();
		xhttp.onreadystatechange = function() {
			if (this.readyState == 4 && this.status == 200) {
				if (this.response != "OK") {
					alert("Update Failed! Please try again.");
				} else {
					document.querySelector("#btnRefreshHealth").classList.remove("is-loading");
				}
			}
		};
		xhttp.open("POST", furl);
		xhttp.send();		
	}

	function refreshFoundation() {
		document.querySelector("#btnRefreshFoundation").classList.add("is-loading");
		var furl = "/admin-refresh-foundation";
		var xhttp=new XMLHttpRequest();
		xhttp.onreadystatechange = function() {
			if (this.readyState == 4 && this.status == 200) {
				if (this.response != "OK") {
					alert("Update Failed! Please try again.");
				} else {
					document.querySelector("#btnRefreshFoundation").classList.remove("is-loading");
				}
			}
		};
		xhttp.open("POST", furl);
		xhttp.send();		
	}
</script>
{% endblock %}
admin_users.html

This is our Admin – Maintain Users page. It provides an interface for creating new users and maintaining existing user.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
<h1>Maintain Users</h1>
<h6>Update Name, Role, Active and Reset Password</h6>
</div>

<table class="container user-table" id="user-table">
    <tr>
        <th>ID</th>
        <th>Email</th>
        <th>Name</th>
        <th>Role</th>
        <th>Active</th>
        <th>Password</th>
    </tr>
{% for user in userData.users %}
  {% if user.active != 1 %}
    {% set visClass = "not-visible" %}
  {% elif user.id == current_user.id %}
    {% set visClass = "curr-user" %}
  {% else %}
    {% set visClass = "" %}
  {% endif %}
    <tr {{ 'class=' + visClass if visClass else null }}>
        <td>{{ user.id }}</td>
        <td>{{ user.email }}</td>
        <td contenteditable>{{ user.name }}</td>
        <td>
            {% if user.id != current_user.id %}
            <select id="role-{{ user.id }}">
                <option value="Admin" {{ 'selected' if user.role == 'Admin' else '' }}>Admin</option>
                <option value="Guest" {{ 'selected' if user.role == 'Guest' else '' }}>Guest</option>
                <option value="User" {{ 'selected' if user.role == 'User' else '' }}>User</option>
            </select></td>
            {% else %}
                {{ user.role }}
            {% endif %}
        <td style="text-align:center;">
            {% if user.id != current_user.id %}
            <input type="checkbox" id="active-{{ user.id }}" name="active-{{ user.id }}" value="active" {{ 'checked' if user.active == 1 else null }}>
            {% else %}
                {{ 'Active' if user.active else 'Inactive' }}
            {% endif %}
        <td style="text-align:center;"> 
            <input type="checkbox" id="reset-{{ user.id }}" name="reset-{{ user.id }}" value="reset">
            <label for="reset-{{ user.id }}">Reset</label>
        </td>
    </tr>
{% endfor %}    
</table>
<div class="section save-container">
    <p>
        <button type="button" class="button is-info is-medium" onclick="addUser()">New</button>
        <button type="button" class="button is-info is-medium" id="btnSave" onclick="saveConfig()">Save</button>
    </p>
</div>

<div class="form-background">
<div id="add-user-form" style="text-align: left !important;">
    <div class="field">
      <label class="label">Email</label>
      <div class="control has-icons-left has-icons-right">
        <input class="input" id="form-email" type="email" placeholder="Email input" value="" autofocus>
        <span class="icon is-small is-left">
          <i class="fas fa-envelope"></i>
        </span>
        <span class="icon is-small is-right">
          <i class="fas fa-check"></i>
        </span>
      </div>
    </div>

    <div class="field">
      <label class="label">Name</label>
      <div class="control">
        <input class="input" id="form-name" type="text" placeholder="Name">
      </div>
    </div>

    <div class="field">
        <label class="label">Role</label>
      <div class="control">
        <label class="select">
            <select id="form-role">
                <option>Admin</option>
                <option>Guest</option>
                <option selected>User</option>
            </select>
      </div>
    </div>

    <div class="field">
      <div class="control">
        <label class="checkbox">
          <input type="checkbox" id="form-active">
          Activate User
        </label>
      </div>
    </div>

    <div class="field">
      <div class="label">Note: Password will be set at first login</div>
    </div>

    <div class="field is-grouped">
      <div class="control">
        <button class="button is-link" id="btnNew" onclick="saveNewUser()">Submit</button>
      </div>
      <div class="control">
        <button class="button is-link is-light" onclick="closeForm()">Cancel</button>
      </div>
    </div>
</div>
</div>

<script>
    document.querySelector("#users-menu").classList.add("active");
    var userData = {{ userData | safe }};
    console.log(JSON.stringify(userData));
    
    const ID = 0;
    const EMAIL = 1;
    const NAME = 2;
    const ROLE = 3;
    const ACTIVE = 4;
    const RESET = 5
    
    var table = document.querySelector("#user-table");

    table.addEventListener("keypress", function(e) {
      if (e.key == "Enter") {
        e.preventDefault();
      }
    });

    table.addEventListener("focusout", function(e) {
        var tableRow = e.target.closest("tr");
        var tableCell = e.target.closest("td");
        var index = tableRow.rowIndex-1;
        var defaultColor = tableRow.cells[0].style.backgroundColor;
        if (tableCell.cellIndex == NAME) {
            if (e.target.innerHTML != userData.users[index].name) {
                e.target.style.backgroundColor = "red";
            } else {
                e.target.style.backgroundColor = defaultColor;
            }
        } else if (tableCell.cellIndex == ROLE) {
            var selected = document.querySelector("#role-" + userData.users[index].id);
            console.log("Selected: " + selected.value);
            if (selected.value != userData.users[index].role) {
                e.target.style.backgroundColor = "red";
            } else {
                e.target.style.backgroundColor = defaultColor;
            }
        } else if (tableCell.cellIndex == ACTIVE) {
            var activeCell = document.querySelector("#active-" + userData.users[index].id);
            console.log("Active: " + activeCell.checked);
            var activeVal = activeCell.checked ? 1 : 0;
            if (activeVal != userData.users[index].active) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }            
        } else if (tableCell.cellIndex == RESET) {
            if (document.querySelector("#reset-" + userData.users[index].id).checked) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        }
    });
    
    function addUser() {
        document.querySelector(".form-background").style.display = "block";
        document.querySelector("#form-email").focus();
    }

    function closeForm() {
        document.querySelector(".form-background").style.display = "none";
    }

    function saveNewUser() {
        var newUser = {};
        var email = document.querySelector("#form-email").value;
        var name = document.querySelector("#form-name").value;
        var role = document.querySelector("#form-role").value;
        var active = document.querySelector("#form-active").checked;
        if (!validateEmail()) {
            document.querySelector("#form-email").focus();
        } else {
            document.querySelector("#btnNew").classList.add("is-loading");
            newUser.email = email;
            newUser.name = name;
            newUser.role = role;
            newUser.active = active ? "1" : "0";
            console.log(JSON.stringify(newUser, null, 2));
            createUser(newUser);
            window.location.reload();
            closeForm();
        }
    }
    
    function validateEmail() {
        return true;
        
        var email = document.querySelector("#form-email").value;
        var mailformat = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
        if(email.match(mailformat)){
            var table = document.querySelector("#user-table");
            var rows = table.rows.length;
            for (var x = 0; x < rows; x++) {
                if (email == table.rows[x].cells[EMAIL].innerHTML) {
                    alert("Email already used!");
                    return false;
                }
            }
            return true;
        } else {
            alert("You have entered an invalid email address!");
            return false;
        }
    }
    
    function saveConfig() {
        document.querySelector("#btnSave").classList.add("is-loading");
        var rows = table.rows.length;
        var userChanges = {"users": []};

        for (row = 1; row < rows; row++) {
            var tableRow = table.rows[row];
            console.log("id: " + userData.users[row-1].id);
            var roleCell = document.querySelector("#role-" + userData.users[row-1].id);
            var activeCell = document.querySelector("#active-" + userData.users[row-1].id);
            var resetCell = document.querySelector("#reset-" + userData.users[row-1].id);
            if (roleCell && activeCell) {
                var activeVal = activeCell.checked ? 1 : 0;
                if (tableRow.cells[NAME].innerHTML != userData.users[row-1].name ||
                    roleCell.value != userData.users[row-1].role ||
                    activeVal != userData.users[row-1].active ||
                    resetCell.checked) {
                    userChanges.users.push({"id": userData.users[row-1].id,
                                               "name": tableRow.cells[NAME].innerHTML,
                                               "role": roleCell.value,
                                               "active": activeVal.toString(),
                                               "reset": resetCell.checked ? "1" : "0"});            
                }
            } else {
                if (tableRow.cells[NAME].innerHTML != userData.users[row-1].name ||
                    resetCell.checked) {
                    userChanges.users.push({"id": userData.users[row-1].id,
                                               "name": tableRow.cells[NAME].innerHTML,
                                               "role": userData.users[row-1].role,
                                               "active": userData.users[row-1].active.toString(),
                                               "reset": resetCell.checked ? "1" : "0"}); 
                }
            }
        }

        console.log(userChanges);

        if (userChanges.users.length > 0) {
          updateUsers(userChanges);
          setTimeout(reloadWindow, 1000);
        } else {
            document.querySelector("#btnSave").classList.Remove("is-loading");
        }
    }

    function reloadWindow() {
        window.location.reload();
    }

    function updateUsers(userChanges) {
        var furl = "/update-users";
        console.log("furl: " + furl);

        var xhttp=new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                if (this.response != "OK") {
                    alert("Update Failed!  Please try again.");
                }
            }
        };
        xhttp.open("POST", furl);
        xhttp.setRequestHeader("Content-Type", "application/json");
        xhttp.send(JSON.stringify(userChanges));
    };

    function createUser(newUser) {
        var furl = "/new-user";
        var xhttp=new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                if (this.response != "OK") {
                    alert("Update Failed! Please try again.");
                }
            }
        };
        xhttp.open("POST", furl);
        xhttp.setRequestHeader("Content-Type", "application/json");
        xhttp.send(JSON.stringify(newUser));
    }
    
</script>
{% endblock %}
admin_logging.html

This is our Admin – Configure Logging page. It will allow you to configure which user events will be logged to the user_login table.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
<h1>Configure Logging</h1>
<h6>Select Events to Log</h6>
</div>
<table class="container logging-table" id="logging-table">
    <tr>
        <th>Event</th>
        <th>Log Event</th>
    </tr>
{% for log in logData.logs %}
    <tr>
        <td>{{ log.event }}</td>
        <td> <input type="checkbox" id="{{ log.event }}" name="{{ log.event }}" value="log" {{ 'checked' if log.log_event == '1' else '' }}>
             <label for="{{ log.event }}">Log</label>
        </td>
    </tr>
{% endfor %}    
</table>
<div class="section save-container">
    <p>
        <button type="button" class="button is-info is-medium" id="btnSave" onclick="saveConfig()">Save</button>
    </p>
</div>

<script>
    document.querySelector("#users-menu").classList.add("active");
    var logData = {{ logData | safe }};
    console.log(JSON.stringify(logData));
    
    const EVENT = 0;
    const LOG_EVENT = 1;
    
    var table = document.querySelector("#logging-table");
/*
    table.addEventListener("keypress", function(e) {
      if (e.key == "Enter") {
        e.preventDefault();
      }
    });
*/
    table.addEventListener("focusout", function(e) {
        var tableRow = e.target.closest("tr");
        var tableCell = e.target.closest("td");
        var index = tableRow.rowIndex-1;
        var defaultColor = tableRow.cells[0].style.backgroundColor;
        if (tableCell.cellIndex == EVENT) {
            if (document.querySelector(logData.logs[index].event).checked) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        }
    });
    
    function saveConfig() {
        document.querySelector("#btnSave").classList.add("is-loading");
        var rows = table.rows.length;

        var logChanges = {"logs": []};

        for (row = 1; row < rows; row++) {
            var tableRow = table.rows[row];
            console.log("event: " + logData.logs[row-1].event + " / " + logData.logs[row-1].log_event);
			var logCell = document.querySelector("#" + logData.logs[row-1].event);
			var logEvent = logCell.checked ? 1 : 0;
			if (logEvent != logData.logs[row-1].log_event) {
				logChanges.logs.push({"id": logData.logs[row-1].id,
									  "log_event": logCell.checked ? "1" : "0"});            
			}
		}

        console.log(logChanges);

        if (logChanges.logs.length > 0) {
          updateConfig(logChanges);
          window.location.reload();
        } else {
            document.querySelector("#btnSave").classList.remove("is-loading");
        }
    }

    function updateConfig(logChanges) {
        var furl = "/update-logging";
        console.log("furl: " + furl);

        var xhttp=new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                if (this.response != "OK") {
                    alert("Update Failed!  Please try again.");
                }
            }
        };
        xhttp.open("POST", furl);
        xhttp.setRequestHeader("Content-Type", "application/json");
        xhttp.send(JSON.stringify(logChanges));
    };
    
</script>
{% endblock %}
admin_failed_login.html

This is our Admin – Failed Login Attempts page. It allows us to see all failed attempts to login to our server. You can delete the contents of the log from this page.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
<h1>Failed Login Attempts</h1>
</div>

<table class="container failed-table" id="failed-table">
    <tr>
        <th>ID</th>
        <th>Email</th>
        <th>Password</th>
        <th>Date</th>
        <th>IP</th>
    </tr>
{% for data in logData.data %}
    <tr>
        <td>{{ data.id }}</td>
        <td>{{ data.email }}</td>
        <td>{{ data.password }}</td>
        <td>{{ data.date }}</td>
        <td>{{ data.ip }}</td>
    </tr>
{% endfor %}    
</table>
<div class="section save-container"><p><button type="button" class="button is-danger is-medium" id="btnDelete" onclick="deleteLog()">Delete</button></p></div>


<script>
    document.querySelector("#users-menu").classList.add("active");
    var logData = {{ logData | safe }};
    console.log(JSON.stringify(logData,null,2));
    
    var table = document.querySelector("#failed-table");

    function deleteLog() {
      if (!confirm("Are you sure you want to delete all records?")) {
	return;
      }

      document.querySelector("#btnDelete").classList.add("is-loading");
      logRecords = {"logs": []}
	  
      logData.data.forEach( log => {
	logRecords.logs.push({"id": log.id});
      });
      deleteLogData(logRecords);
      window.location.reload();
    }

    function deleteLogData(logData) {
      var furl = "/delete-failed-login";
      var xhttp=new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
	if (this.readyState == 4 && this.status == 200) {
	  if (this.response != "OK") {
	    alert("Update Failed! Please try again.");
	  }
	}
      };
      xhttp.open("POST", furl);
      xhttp.setRequestHeader("Content-Type", "application/json");
      xhttp.send(JSON.stringify(logData));
    }
</script>
{% endblock %}
admin_logs.html

This is our Admin – User Logs page. You can view all user events that have been logged to the user_login table here. You can also delete the contents of the log from this page.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
<h1>User Logs</h1>
</div>

<table class="container log-table" id="log-table">
    <tr>
        <th>ID</th>
        <th>User ID</th>
        <th>Email</th>
        <th>Event</th>
        <th>Date</th>
        <th>IP</th>
    </tr>
{% for log in logData.logs %}
    <tr>
        <td>{{ log.id }}</td>
        <td>{{ log.user_id }}</td>
        <td>{{ log.email }}</td>
        <td>{{ log.event }}</td>
        <td>{{ log.date }}</td>
        <td>{{ log.ip }}</td>
    </tr>
{% endfor %}    
</table>
<div class="section save-container"><p><button type="button" class="button is-danger is-medium" id="btnDelete" onclick="deleteLogs()">Delete</button></p></div>


<script>
    document.querySelector("#users-menu").classList.add("active");
    var logData = {{ logData | safe }};
    console.log(JSON.stringify(logData,null,2));
    
    var table = document.querySelector("#log-table");
    
    function deleteLogs() {
      if (!confirm("Are you sure you want to delete all records?")) {
	return;
      }
      
      document.querySelector("#btnDelete").classList.add("is-loading");      
      var logRecords = {"logs": []};

      logData.logs.forEach(log => {
	logRecords.logs.push({"id": log.id});
      });
      deleteLogData(logRecords);
      window.location.reload();
    }

    function deleteLogData(logData) {
      var furl = "/delete-user-logs";
      var xhttp=new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
	if (this.readyState == 4 && this.status == 200) {
	  if (this.response != "OK") {
	    alert("Update Failed! Please try again.");
	  }
	}
      };
      xhttp.open("POST", furl);
      xhttp.setRequestHeader("Content-Type", "application/json");
      xhttp.send(JSON.stringify(logData));
    }

</script>
{% endblock %}
admin_rooms.html

This is our Admin – Rooms Configuration page. It will allow you to configure the following:

Location: Create a nickname for the location that will be used for the SHD display. Create an email association for the location – for future use.

Rooms: Define if a room will be visible on the dashboard and what sequence it will be displayed in. Guest Access will define if the room will be visible to Guest users.

Devices: Define if a device will be visible on the dashboard and what sequence it will be displayed in. Guest Access will define if a Guest user will be able to execute commands for that device. Icon will allow you to define an alternate Font Awesome icon for that device.

Capabilities: Define if a device’s capability will be visible on the dashboard.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
  <div class="content">
  <h1>Rooms Configuration</h1>
  <h6>Update 'Seq' to change display sequence<br />
  Update 'Visible' to show/hide items or groups<br />
  Update 'Guest Access' to allow Guest Users access</h6>
  </div>
  
  <div class="location-data" style="text-align:left !important;">
    <div>Location Name: [<span style="background-color: white;">{{ configData.location.name }}</span>]</div>
    <div>Location Nickname: [<span class="unchanged" id="location-nickname">{{ configData.location.nickname }}</span>] <span><i class='fas fa-edit unchanged' id="edit-nickname" style='font-size: 20px;' onclick='editNickname()'></i></span></div>
    <div>Location Email: [<span class="unchanged" id="location-email">{{ configData.location.email }}</span>] <span><i class='fas fa-edit unchanged' id="edit-email" style='font-size: 20px;' onclick='editEmail()'></i></span></div>
  </div>
  <table class="container" id="room-table">
      <tr class="room-header">
          <th></th>
          <th>Room</th>
          <th>Seq</th>
          <th>Visible</th>
          <th>Guest Access</th>
          <th></th>
          <th></th>
      </tr>
  {% for room in configData.rooms %}
    {% set roomIdx = loop.index0 %}
    {% if room.visible != 1 %}
        {% set visClass = "not-visible" %}
    {% else %}
        {% set visClass = "" %}
    {% endif %}
    <tr class="room {{ visClass }}" id="room-{{ loop.index0 }}">
        <td style='cursor:pointer;' onclick='toggle("{{room.name }}")'> +/- </td>
        <td>{{ room.name }}</td>
        <td style="text-align:center; min-width: 70px;">
            <input type="number" id="seq-{{ room.room_id }}" min="0" max="99" value="{{ room.seq }}">
        </td>
        <td style="text-align:center;">
            <input type="checkbox" id="visible-{{ room.room_id }}" name="visible-{{ room.room_id }}" value="visible" {{ 'checked' if room.visible == 1 else null }}>
        </td>
        <td style="text-align:center;">
            <input type="checkbox" id="guest-{{ room.room_id }}" name="guest-{{ room.room_id }}" value="guest" {{ 'checked' if room.guest_access == 1 else null }}>
        </td>
        <td></td>
        <td></td>
    </tr>
    
    <tr class="device-header {{ visClass }}">
        <th></th>
        <th></th>
        <th>Device</th>
        <th>Seq</th>
        <th>Visible</th>
        <th>Guest Access</th>
        <th>Icon</th>
    </tr>
    {% for device in room.devices %}
        {% set deviceIdx = loop.index0 %}
        {% if room.visible != 1 or device.visible != 1 %}
            {% set visClass = "not-visible" %}
        {% else %}
            {% set visClass = "" %}
        {% endif %}
        <tr class="device {{ visClass }}" data-room="{{ roomIdx }}" id="device-{{ loop.index0 }}">
            <td></td>
            <td></td>
            <td>{{ device.label }}</td>
            <td style="text-align:center; min-width: 70px;">
                <input type="number" id="seq-{{ device.device_id }}" min="0" max="99" value="{{ device.seq }}">
            </td>
            <td style="text-align:center;">
                <input type="checkbox" id="visible-{{ device.device_id }}" name="visible-{{ device.device_id }}" value="visible" {{ 'checked' if device.visible == 1 else null }}>
            </td>
            <td style="text-align:center;">
                <input type="checkbox" id="guest-{{ device.device_id }}" name="guest-{{ device.device_id }}" value="guest" {{ 'checked' if device.guest_access == 1 else null }}>
            </td>
            <td contenteditable>{{ device.icon }}</td>
        </tr>
        <tr class="capability-header {{ visClass }}">
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th>Capability</th>
            <th>Visible</th>
        </tr>
        {% for capability in device.capabilities %}
            {% if room.visible != 1 or device.visible != 1 or capability.visible != 1 %}
                {% set visClass = "not-visible" %}
            {% else %}
                {% set visClass = "" %}
            {% endif %}
            <tr class="capability {{ visClass }}" data-room="{{ roomIdx }}" data-device="{{ deviceIdx }}" id="capability-{{ loop.index0 }}">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td>{{ capability.capability_id }}</td>
                <td style="text-align:center;">
                    <input type="checkbox" id="visible-{{ device.device_id }}-{{capability.capability_id }}" name="visible-{{ device.device_id }}-{{capability.capability_id }}" value="visible" {{ 'checked' if capability.visible == 1 else null }}>
                </td>
            </tr>
        {% endfor %}
    {% endfor %}
    
    
  {% endfor %}
  </table>
  <div class="section save-container"><p><button type="button" class="button is-info is-medium" id="btnSave" onclick="saveConfig()">Save</button></p></div>
  
  <script>
    document.querySelector("#rooms-menu").classList.add("active");
      
    const ROOM_NAME = 1;
    const ROOM_SEQ = 2;
    const ROOM_VIS = 3;
    const ROOM_GUEST = 4;
    const DEVICE_LABEL = 2;
    const DEVICE_SEQ = 3;
    const DEVICE_VIS = 4;
    const DEVICE_GUEST = 5;
    const DEVICE_ICON = 6;
    const CAPABILITY_ID = 5;
    const CAPABILITY_VIS = 6;
    
    disp = document.querySelector("#configs");
    var configData = {{ configData | safe }};
    console.log(JSON.stringify(configData, null, 2));
    
    var table = document.querySelector("#room-table");
    table.addEventListener("keypress", function(e) {
      if (e.key == "Enter") {
        e.preventDefault();
      }
    });

    table.addEventListener("focusout", function(e) {
      var tableRow = e.target.closest("tr");
      var tableCell = e.target.closest("td");
      var defaultColor = tableRow.cells[0].style.backgroundColor;

      if (tableRow.classList.contains("room")) {
        var roomId = tableRow.id;
        var roomIdx = parseInt(roomId.substring(roomId.indexOf("-")+1));
        var room = configData.rooms[roomIdx];
        var cellSeq = document.querySelector("#seq-" + room.room_id);
        var cellVisible = document.querySelector("#visible-" + room.room_id);
        var cellVisibleVal = cellVisible.checked ? 1 : 0;
        var cellGuest = document.querySelector("#guest-" + room.room_id);
        var cellGuestVal = cellGuest.checked ? 1 : 0;

        if (tableCell.cellIndex == ROOM_SEQ) {
          if (room.seq != cellSeq.value) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        } else if (tableCell.cellIndex == ROOM_VIS) {
          if (room.visible != cellVisibleVal) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        } else if (tableCell.cellIndex == ROOM_GUEST) {
          if (room.guest_access != cellGuestVal) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        }
      } else if (tableRow.classList.contains("device")) {
        var roomIdx = tableRow.getAttribute("data-room");
        var deviceId = tableRow.id
        var deviceIdx = parseInt(deviceId.substring(deviceId.indexOf("-")+1));
        var device = configData.rooms[roomIdx].devices[deviceIdx];
        var cellSeq = document.querySelector("#seq-" + device.device_id);
        var cellVisible = document.querySelector("#visible-" + device.device_id);
        var cellVisibleVal = cellVisible.checked ? 1 : 0;
        var cellGuest = document.querySelector("#guest-" + device.device_id);
        var cellGuestVal = cellGuest.checked ? 1 : 0;

        if (tableCell.cellIndex == DEVICE_SEQ) {
          if (device.seq != cellSeq.value) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        } else if (tableCell.cellIndex == DEVICE_VIS) {
          if (device.visible != cellVisibleVal) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        } else if (tableCell.cellIndex == DEVICE_GUEST) {
          if (device.guest_access != cellGuestVal) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        } else if (tableCell.cellIndex == DEVICE_ICON) {
          console.log("Icon: " + tableCell.innerText);
          if (device.icon != tableCell.innerHTML) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        }
      } else if (tableRow.classList.contains("capability")) {
        var roomIdx = tableRow.getAttribute("data-room");
        var deviceIdx = tableRow.getAttribute("data-device");
        var device = configData.rooms[roomIdx].devices[deviceIdx];
        var capabilityId = tableRow.id
        var capabilityIdx = parseInt(capabilityId.substring(capabilityId.indexOf("-")+1));
        var capability = device.capabilities[capabilityIdx];
        var cellVisible = document.querySelector("#visible-" + device.device_id + "-" + capability.capability_id);
        var cellVisibleVal = cellVisible.checked ? 1 : 0;
        
        if (tableCell.cellIndex == CAPABILITY_VIS) {
          if (capability.visible != cellVisibleVal) {
            tableCell.style.backgroundColor = "red";
          } else {
            tableCell.style.backgroundColor = defaultColor;
          }
        }
      }
    });

    function editNickname() {
      nickname = document.querySelector("#location-nickname");
      editNicknameBtn = document.querySelector("#edit-nickname");
      
      var name = prompt("Enter a nickname for this location:", nickname.innerHTML);
      if (name != null) {
        if (nickname.innerHTML != name) {
          nickname.innerHTML = name;
          if (name != configData.location.nickname) {
            nickname.classList.remove("unchanged");
            nickname.classList.add("changed");
            editNicknameBtn.classList.remove("unchanged");
            editNicknameBtn.classList.add("changed");
          } else {
            nickname.classList.remove("changed");
            nickname.classList.add("unchanged");
            editNicknameBtn.classList.remove("changed");
            editNicknameBtn.classList.add("unchanged");
          }
        }
      }
    }
    
    function editEmail() {
      email = document.querySelector("#location-email");
      editEmailBtn = document.querySelector("#edit-email");
      
      var name = prompt("Enter an email address for this location:", email.innerHTML);
      if (name != null) {
        if (email.innerHTML != name) {
          email.innerHTML = name;
          if (name != configData.location.email) {
            email.classList.remove("unchanged");
            email.classList.add("changed");
            editEmailBtn.classList.remove("unchanged");
            editEmailBtn.classList.add("changed");
          } else {
            email.classList.remove("changed");
            email.classList.add("unchanged");
            editEmailBtn.classList.remove("changed");
            editEmailBtn.classList.add("unchanged");
          }
        }
      }
    }
    
    function toggle(element) {
        console.log("Clicked on " + element);
        var rows = table.rows.length;
        console.log("Table rows: " + rows);
        var found = false;
        var display = "none";
        for (row = 0; row < rows; row++) {
            var room = table.rows[row].cells[1].innerHTML;
            if (room.length == 0 && found) {
                table.rows[row].style.display = display;
            } else {
                found = false;
            }
            if (room == element) {
                console.log("Found [" + room + "] at row " + row);
                found = true;
                if (row < rows-1) {
                    var dispVal = window.getComputedStyle(table.rows[row+1], null).getPropertyValue("display");
                    console.log("Display [" + dispVal + "]");
                    if (dispVal == "none") {
                        display = "table-row";
                    } else {
                        display = "none";
                    }                    
                }
            }
        }
        
    }
    
    function saveConfig() {
        document.querySelector("#btnSave").classList.add("is-loading");
        var table = document.querySelector("#room-table");
        var rows = table.rows.length;
        var configChanges = {"location": [], "rooms": [], "devices": [], "capabilities": []};

        var nickname = document.querySelector("#location-nickname");
        var email = document.querySelector("#location-email");
        if (nickname.innerHTML != configData.location.nickname ||
            email.innerHTML != configData.location.email) {
              nicknameVal = nickname.innerHTML;
              emailVal = email.innerHTML;
          configChanges.location.push({"location_id": configData.location.location_id});
          configChanges.location.push({"nickname": nicknameVal});
          configChanges.location.push({"email": emailVal});
        }
        
        var roomIdx = 0;
        var deviceIdx = 0;
        var capabilityIdx = 0;
        for (row = 1; row < rows; row++) {
            var tableRow = table.rows[row];
            if (table.rows[row].classList.contains("room")) {
                var room = tableRow.cells[ROOM_NAME];
                var roomId = tableRow.id;
                roomIdx = parseInt(roomId.substring(roomId.indexOf("-")+1))
                var room = configData.rooms[roomIdx];
                var cellSeq = document.querySelector("#seq-" + room.room_id);
                var cellVisible = document.querySelector("#visible-" + room.room_id);
                var cellVisibleVal = cellVisible.checked ? 1 : 0;
                var cellGuest = document.querySelector("#guest-" + room.room_id);
                var cellGuestVal = cellGuest.checked ? 1 : 0;

                if (room.seq != cellSeq.value ||
                    room.visible != cellVisibleVal ||
                    room.guest_access != cellGuestVal) {
                  var change = {"room_id": room.room_id}
                  change.seq = cellSeq.value;
                  change.visible = cellVisibleVal;
                  change.guest_access = cellGuestVal;
                  configChanges.rooms.push(change);
                }
            } else if (tableRow.classList.contains("device")) {
                var device = tableRow.cells[DEVICE_LABEL];
                var deviceId = tableRow.id;
                deviceIdx = parseInt(deviceId.substring(deviceId.indexOf("-")+1));
                var device = configData.rooms[roomIdx].devices[deviceIdx];
                var cellSeq = document.querySelector("#seq-" + device.device_id);
                var cellVisible = document.querySelector("#visible-" + device.device_id);
                var cellVisibleVal = cellVisible.checked ? 1 : 0;
                var cellGuest = document.querySelector("#guest-" + device.device_id);
                var cellGuestVal = cellGuest.checked ? 1 : 0;
                var cellIcon = tableRow.cells[DEVICE_ICON];

                if (device.seq != cellSeq.value ||
                    device.visible != cellVisibleVal ||
                    device.guest_access != cellGuestVal ||
                    device.icon != cellIcon.innerHTML) {
                        var change = {"device_id": device.device_id};
                        change.seq = cellSeq.value;
                        change.visible = cellVisibleVal;
                        change.guest_access = cellGuestVal;
                        change.icon = cellIcon.innerText;
                        configChanges.devices.push(change);
                }
            } else if (tableRow.classList.contains("capability")) {
                var capability = tableRow.cells[CAPABILITY_ID];
                var capabilityId = tableRow.id;
                capabilityIdx = parseInt(capabilityId.substring(capabilityId.indexOf("-")+1));
                var device = configData.rooms[roomIdx].devices[deviceIdx];
                var capability = device.capabilities[capabilityIdx];
                var cellVisible = document.querySelector("#visible-" + device.device_id + "-" + capability.capability_id);
                var cellVisibleVal = cellVisible.checked ? 1 : 0;

                if (capability.visible != cellVisibleVal) {
                      var change = {"device_id": device.device_id,
                            "capability_id": capability.capability_id};
                      change.visible = cellVisibleVal;
                      configChanges.capabilities.push(change);
                }
            }
        }
        console.log(configChanges);
        if (configChanges.location.length > 0 || configChanges.rooms.length > 0 ||
            configChanges.devices.length > 0 || configChanges.capabilities.length > 0) {
          updateConfigs(configChanges);
          window.location.reload();
        } else {
            document.querySelector("#btnSave").classList.remove("is-loading");
        }
    }

		function updateConfigs(configChanges) {
			var furl = "/update-room-configs";
			console.log("furl: " + furl);

			var xhttp=new XMLHttpRequest();
			xhttp.onreadystatechange = function() {
				if (this.readyState == 4 && this.status == 200) {
					if (this.response != "OK") {
						alert("Update Failed!  Please try again.");
					}
				}
			};
			xhttp.open("POST", furl);
      xhttp.setRequestHeader("Content-Type", "application/json");
			xhttp.send(JSON.stringify(configChanges));
		};
  </script>

{% endblock %}
admin_presence.html

This is our Admin – Presence Configuration page. It will allow you to define which presence sensors are displayed on your SHD and in what order. You can also optionally define a Nickname for each presence sensor to override the default display name.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
<h1>Presence Configuration</h1>
<h6>Update 'Nickname' to change display name<br />
Update 'Seq' to change display sequence<br />
Update 'Visible' to hide/display items<br />
</div>

<table class="container presence-table" id="presence-table">
    <tr>
        <th>Sensor</th>
        <th>Nickname</th>
        <th>Seq</th>
        <th>Visible</th>
    </tr>
{% for sensor in configData.presence %}
  {% if sensor.visible != 1 %}
    {% set visClass = "not-visible" %}
  {% else %}
    {% set visClass = "" %}
  {% endif %}
    <tr class="{{ visClass }}">
        <td>{{ sensor.label }}</td>
        <td contenteditable>{{ sensor.nickname }}</td>
        <td style="text-align:center; min-width:70px;">
            <input type="number" id="seq-{{ sensor.device_id }}" min="0" max="99" value="{{ sensor.seq }}">
        </td>
        <td style="text-align:center;">
            <input type="checkbox" id="visible-{{ sensor.device_id }}" name="visible-{{ sensor.device_id }}" value="visible" {{ 'checked' if sensor.visible == 1 else null }}>
        </td>
    </tr>
{% endfor %}    
</table>
<div class="section save-container"><p><button type="button" class="button is-info is-medium" id="btnSave" onclick="saveConfig()">Save</button></p></div>


<script>
    document.querySelector("#presence-menu").classList.add("active");
    var configData = {{ configData | safe }};

    
    const NAME = 0;
    const NICKNAME = 1;
    const SEQ = 2;
    const VISIBLE = 3;
    
    var table = document.querySelector("#presence-table");

    table.addEventListener("keypress", function(e) {
      if (e.key == "Enter") {
        e.preventDefault();
      }
    });

    table.addEventListener("focusout", function(e) {
        var tableRow = e.target.closest("tr");
        var tableCell = e.target.closest("td");
        var index = tableRow.rowIndex-1;
        var sensor = configData.presence[index];
        var cellSeq = document.querySelector("#seq-" + sensor.device_id);
        var cellVisible = document.querySelector("#visible-" + sensor.device_id);
        var cellVisibleVal = cellVisible.checked ? 1 : 0;
        var defaultColor = tableRow.cells[0].style.backgroundColor;
        console.log("Row: " + index);
        if (tableCell.cellIndex == NICKNAME) {
            if (sensor.nickname != tableCell.innerText) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        } else if (tableCell.cellIndex == SEQ) {
            if (sensor.seq != cellSeq.value) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        } else if (tableCell.cellIndex == VISIBLE) {
            if (sensor.visible != cellVisibleVal) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        }
    });
    
    
    function saveConfig() {
        document.querySelector("#btnSave").classList.add("is-loading");
        var rows = table.rows.length;
        var configChanges = {"presence": []};

        for (row = 1; row < rows; row++) {
            var tableRow = table.rows[row];
            var sensor = configData.presence[row-1];
            var cellSeq = document.querySelector("#seq-" + sensor.device_id);
            var cellVisible = document.querySelector("#visible-" + sensor.device_id);
            var cellVisibleVal = cellVisible.checked ? 1 : 0;
            
            if (tableRow.cells[NICKNAME] != sensor.nickname ||
                cellSeq.value != sensor.seq ||
                cellVisibleVal != sensor.visible) {
                configChanges.presence.push({"device_id": sensor.device_id,
                                           "nickname": tableRow.cells[NICKNAME].innerText,
                                           "seq": cellSeq.value,
                                           "visible": cellVisibleVal});
            
            }
        }

        console.log(configChanges);

        if (configChanges.presence.length > 0) {
          updateConfigs(configChanges);
          window.location.reload();
        } else {
            document.querySelector("#btnSave").classList.remove("is-loading");
        }
    }

    function updateConfigs(configChanges) {
        var furl = "/update-presence-configs";
        console.log("furl: " + furl);

        var xhttp=new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                if (this.response != "OK") {
                    alert("Update Failed!  Please try again.");
                }
            }
        };
        xhttp.open("POST", furl);
        xhttp.setRequestHeader("Content-Type", "application/json");
        xhttp.send(JSON.stringify(configChanges));
    };
    
</script>
{% endblock %}
admin_scenes.html

This is our Admin – Scenes Configuration page. It will allow you to define which scenes are displayed on your SHD and in what order.

Save this file in your project templates folder (i.e., /home/pi/st_webhook/templates)

{% extends "admin_base.html" %}

{% block content %}
<div class="content">
<h1>Scenes Configuration</h1>
<h6>Update 'Seq' to change display sequence<br />
Update 'Visible' to hide/display items<br />
Update 'Guest Access' to allow Guest Users to run scene</h6>
</div>

<table class="container scene-table" id="scene-table">
    <tr>
        <th>Scene</th>
        <th>Seq</th>
        <th>Visible</th>
        <th>Guest Access</th>
    </tr>
{% for scene in configData.scenes %}
  {% if scene.visible != 1 %}
    {% set visClass = "not-visible" %}
  {% else %}
    {% set visClass = "" %}
  {% endif %}
    <tr class="{{ visClass }}">
        <td>{{ scene.name }}</td>
        <td style="text-align:center; min-width:70px;">
            <input type="number" id="seq-{{ scene.scene_id }}" min="0" max="99" value="{{ scene.seq }}">
        </td>
        <td style="text-align:center;">
            <input type="checkbox" id="visible-{{ scene.scene_id }}" name="visible-{{ scene.scene_id }}" value="visible" {{ 'checked' if scene.visible == 1 else null }}>
        </td>
        <td style="text-align:center;">
            <input type="checkbox" id="guest-{{ scene.scene_id }}" name="guest-{{ scene.scene_id }}" value="guest" {{ 'checked' if scene.guest_access == 1 else null }}>
        </td>
    </tr>
{% endfor %}    
</table>
<div class="section save-container"><p><button type="button" class="button is-info is-medium" id="btnSave" onclick="saveConfig()">Save</button></p></div>


<script>
    document.querySelector("#scenes-menu").classList.add("active");
    var configData = {{ configData | safe }};

    
    const NAME = 0;
    const SEQ = 1;
    const VISIBLE = 2;
    const GUEST = 3;
    
    var table = document.querySelector("#scene-table");

    table.addEventListener("keypress", function(e) {
      if (e.key == "Enter") {
        e.preventDefault();
      }
    });

    table.addEventListener("focusout", function(e) {
        var tableRow = e.target.closest("tr");
        var tableCell = e.target.closest("td");
        var index = tableRow.rowIndex-1;
        var cellSeq = document.querySelector("#seq-" + configData.scenes[index].scene_id);
        var cellVisible = document.querySelector("#visible-" + configData.scenes[index].scene_id);
        var cellVisibleVal = cellVisible.checked ? 1 : 0;
        var cellGuest = document.querySelector("#guest-" + configData.scenes[index].scene_id);
        var cellGuestVal = cellGuest.checked ? 1 : 0;
        var defaultColor = tableRow.cells[0].style.backgroundColor;
        console.log("Row: " + index);
        if (tableCell.cellIndex == SEQ) {
            if (configData.scenes[index].seq != cellSeq.value) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        } else if (tableCell.cellIndex == VISIBLE) {
            if (configData.scenes[index].visible != cellVisibleVal) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        } else if (tableCell.cellIndex == GUEST) {
            if (configData.scenes[index].guest_access != cellGuestVal) {
                tableCell.style.backgroundColor = "red";
            } else {
                tableCell.style.backgroundColor = defaultColor;
            }
        }
    });
    
    
    function saveConfig() {
        document.querySelector("#btnSave").classList.add("is-loading");
        var rows = table.rows.length;
        var configChanges = {"scenes": []};

        for (row = 1; row < rows; row++) {
            var tableRow = table.rows[row];
            var cellSeq = document.querySelector("#seq-" + configData.scenes[row-1].scene_id);
            var cellVisible = document.querySelector("#visible-" + configData.scenes[row-1].scene_id);
            var cellVisibleVal = cellVisible.checked ? 1 : 0;
            var cellGuest = document.querySelector("#guest-" + configData.scenes[row-1].scene_id);
            var cellGuestVal = cellGuest.checked ? 1 : 0;
            
            if (cellSeq.value != configData.scenes[row-1].seq ||
                cellVisibleVal != configData.scenes[row-1].visible ||
                cellGuestVal != configData.scenes[row-1].guest_access) {
                configChanges.scenes.push({"scene_id": configData.scenes[row-1].scene_id,
                                           "seq": cellSeq.value,
                                           "visible": cellVisibleVal,
                                           "guest_access": cellGuestVal});
            
            }
        }

        console.log(configChanges);

        if (configChanges.scenes.length > 0) {
          updateConfigs(configChanges);
          window.location.reload();
        } else {
            document.querySelector("#btnSave").classList.remove("is-loading");
        }
    }

    function updateConfigs(configChanges) {
        var furl = "/update-scene-configs";
        console.log("furl: " + furl);

        var xhttp=new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                if (this.response != "OK") {
                    alert("Update Failed!  Please try again.");
                }
            }
        };
        xhttp.open("POST", furl);
        xhttp.setRequestHeader("Content-Type", "application/json");
        xhttp.send(JSON.stringify(configChanges));
    };
    
</script>
{% endblock %}

Start the server!

Now that you have all of the code saved, you can start your server!

If you run into errors, make sure you have all of the libraries installed as listed above and that you’ve completed the prerequisite steps also listed above. If you still have problems, shoot me a message and I’ll try to help you out!

cd /home/pi/st_webhook
python3 st_webhook.py

You should now be able to point a browser at your webserver and see your SHD! If you haven’t created a new user, you can login with the user jeff@example.com/Password to get started. It’s an Admin user so you can go to the Admin console and start adding your own users and customizing your display.

Note: You’ll want to deactivate the jeff@example.com user as soon as you get your own Admin user created.

Summary

I know this was a long post, but I’m hoping it was worth your time. If all went well, you should now have a fully functional Smart Home Dashboard.

Questions?

If you have any questions or need any help, please don’t hesitate to contact me or leave a comment. If I can help, I will gladly do so! You may also find answers in reviewing the previous posts in this series.

What’s next?

Next I will be adding support for multiple locations. If your account is associated with multiple locations and you want to run the SHD at all locations, you’ll need this. Here’s the link!

I will also be adding weather data to my dashboard, but I need to investigate whether or not my custom device-type handler (DTH) can be used by others before I add it to this series. Hopefully I can share it.

See you next time!

Leave a Comment

Your email address will not be published. Required fields are marked *