diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9bea433
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+
+.DS_Store
diff --git a/BackgroundTemplate.afphoto b/BackgroundTemplate.afphoto
new file mode 100644
index 0000000..b8437af
Binary files /dev/null and b/BackgroundTemplate.afphoto differ
diff --git a/Core.js b/Core.js
new file mode 100755
index 0000000..a767883
--- /dev/null
+++ b/Core.js
@@ -0,0 +1,289 @@
+'use strict'
+const UTIL = require('./core/util');
+
+// Check Fresh Environment ?
+UTIL.checkNewEV();
+
+const FS = require('fs');
+const CHALK = require('chalk');
+const SERVER = require('./core/server');
+const ACCESSORY = require('./core/accessory');
+const CONFIG = require(UTIL.ConfigPath);
+const IP = require("ip");
+const MQTT = require('./core/mqtt');
+const PATH = require('path');
+const NODECLEANUP = require('node-cleanup');
+const ROUTING = require('./core/routing');
+
+// resgister process exit handler
+NODECLEANUP(clean);
+
+// Cleanup our mess
+function clean(exitCode, signal) {
+
+ cleanEV().then(() => {
+ process.kill(process.pid, signal);
+ });
+
+ NODECLEANUP.uninstall();
+ return false;
+}
+
+function cleanEV() {
+
+ return new Promise((resolve, reject) => {
+ console.info(' Unpublishing Accessories...');
+ Bridge.unpublish(false);
+
+ const AccessoryIDs = Object.keys(Accesories);
+ for (let i = 0; i < AccessoryIDs.length; i++) {
+ const Acc = Accesories[AccessoryIDs[i]];
+ if (!Acc._Config.bridged) {
+ Acc.unpublish(false);
+ }
+ }
+
+ console.info(' Saving current Characteristics...');
+ const CharacteristicCache = {};
+
+ for (let i = 0; i < AccessoryIDs.length; i++) {
+ const Acc = Accesories[AccessoryIDs[i]];
+ CharacteristicCache[AccessoryIDs[i]] = Acc.getProperties();
+ }
+
+ UTIL.saveCharacteristicCache(CharacteristicCache);
+
+ console.info(' Cleaning up Routes...');
+ let RouteKeys = Object.keys(Routes);
+ RouteKeys.forEach((AE) =>{
+ Routes[AE].close('appclose');
+ })
+
+ resolve();
+ });
+}
+
+// Check if we are being asked for a Reset.
+if (UTIL.checkReset()) {
+ return; // stop (whilst we check they know what they are doing.)
+}
+
+// Check password reset
+if (UTIL.checkPassword()) {
+ return; // stop
+}
+
+// Set routing module path
+ROUTING.setPath(UTIL.RootPath)
+
+// Check install module
+if (UTIL.checkInstallRequest()) {
+ return; // stop
+}
+
+// Banner
+console.log(CHALK.keyword('orange')(" HomeKit"))
+console.log(CHALK.keyword('white')(" Device Stack"))
+console.log(CHALK.keyword('white')(" For the Smart Home Enthusiast, For the curious."))
+console.log(CHALK.keyword('orange')(" _________________________________________________________________"))
+console.log(" ")
+
+// install modules if needed
+ROUTING.installStockModules();
+ROUTING.loadModules();
+
+if (!CONFIG.bridgeConfig.hasOwnProperty("pincode")) {
+
+ // Genertae a Bridge
+ CONFIG.bridgeConfig.pincode = UTIL.getRndInteger(100, 999) + "-" + UTIL.getRndInteger(10, 99) + "-" + UTIL.getRndInteger(100, 999);
+ CONFIG.bridgeConfig.username = UTIL.genMAC();
+ CONFIG.bridgeConfig.setupID = UTIL.makeID(4);
+ CONFIG.bridgeConfig.serialNumber = UTIL.makeID(12)
+ UTIL.saveBridgeConfig(CONFIG.bridgeConfig)
+
+ // Create a demo accessory for new configs (accessories will heronin be created via the ui)
+ const DemoAccessory = {
+ "type": "SWITCH",
+ "name": "Switch Accessory Demo",
+ "route": "Output To Console",
+ "pincode": UTIL.getRndInteger(100, 999) + "-" + UTIL.getRndInteger(10, 99) + "-" + UTIL.getRndInteger(100, 999),
+ "username": UTIL.genMAC(),
+ "setupID": UTIL.makeID(4),
+ "serialNumber": UTIL.makeID(12),
+ "bridged": true
+ }
+ CONFIG.accessories.push(DemoAccessory)
+ UTIL.appendAccessoryToConfig(DemoAccessory)
+}
+
+console.log(" Configuring Homekit Bridge")
+
+// Configure Our Bridge
+const Bridge = new ACCESSORY.Bridge(CONFIG.bridgeConfig)
+Bridge.on('PAIR_CHANGE', Paired)
+Bridge.on('LISTENING', getsetupURI)
+
+// Routes
+const Routes = {}
+
+function setupRoutes() {
+
+ const Keys = Object.keys(Routes);
+
+ for (let i = 0; i < Keys.length; i++) {
+ Routes[Keys[i]].close('reconfigure')
+ delete Routes[Keys[i]];
+ }
+
+ const RouteNames = Object.keys(CONFIG.routes);
+
+ for (let i = 0; i < RouteNames.length; i++) {
+
+ let RouteCFG = CONFIG.routes[RouteNames[i]]
+ console.log(" Configuring Route : " + RouteNames[i] + " (" + RouteCFG.type + ")")
+
+ let RouteClass = new ROUTING.Routes[RouteCFG.type].Class(RouteCFG);
+
+ Routes[RouteNames[i]] = RouteClass;
+
+ }
+}
+
+// This is also called externally (i.e when updating routes via the UI)
+setupRoutes();
+
+// Load up cache (if available)
+var Cache = UTIL.getCharacteristicCache();
+
+// Configure Our Accessories
+const Accesories = {}
+for (let i = 0; i < CONFIG.accessories.length; i++) {
+
+ let AccessoryOBJ = CONFIG.accessories[i]
+ console.log(" Configuring Accessory : " + AccessoryOBJ.name + " (" + AccessoryOBJ.type + ")")
+ AccessoryOBJ.accessoryID = AccessoryOBJ.username.replace(/:/g, "");
+ let Type = ACCESSORY.Types.filter(C => C.Name == AccessoryOBJ.type)[0]
+ let Acc = new Type.Class(AccessoryOBJ);
+
+ if (Cache != null) {
+ if (Cache.hasOwnProperty(AccessoryOBJ.accessoryID)) {
+ console.log(" Restoring Characteristics...")
+ Acc.setCharacteristics(Cache[AccessoryOBJ.accessoryID]);
+ }
+ }
+
+ Acc.on('STATE_CHANGE', (PL, O) => Change(PL, AccessoryOBJ, O))
+ Acc.on('IDENTIFY', (P) => Identify(P, AccessoryOBJ))
+
+ Accesories[AccessoryOBJ.accessoryID] = Acc;
+
+ if (!AccessoryOBJ.bridged) {
+
+ Acc.on('PAIR_CHANGE', (P) => Pair(P, AccessoryOBJ))
+ console.log(" Pin Code " + AccessoryOBJ.pincode)
+ console.log(" Publishing Accessory (Unbridged)")
+ Acc.publish();
+ } else {
+ Bridge.addAccessory(Acc.getAccessory())
+ }
+}
+
+// Publish Bridge
+console.log(" Publishing Bridge")
+Bridge.publish();
+
+console.log(" Starting Client Services")
+
+// Web Server (started later)
+const UIServer = new SERVER.Server(Accesories, Change, Identify, Bridge, setupRoutes, Pair);
+
+// MQTT Client (+ Start Server)
+const MQTTC = new MQTT.MQTT(Accesories, MQTTDone)
+
+function MQTTDone() {
+ UIServer.Start(UIServerDone)
+}
+
+// Server Started
+function UIServerDone() {
+ const BridgeFileName = PATH.join(UTIL.HomeKitPath, "AccessoryInfo." + CONFIG.bridgeConfig.username.replace(/:/g, "") + ".json");
+ if (FS.existsSync(BridgeFileName)) {
+ const IsPaired = Object.keys(require(BridgeFileName).pairedClients)
+ UIServer.setBridgePaired(IsPaired.length > 0);
+ } else {
+ UIServer.setBridgePaired(false);
+ }
+
+ // All done.
+
+ var IPAddress = IP.address();
+ if (CONFIG.webInterfaceAddress != 'ALL') {
+ IPAddress = CONFIG.webInterfaceAddress
+ }
+
+ const Address = CHALK.keyword('red')("http://" + IPAddress + ":" + CONFIG.webInterfacePort + "/ui/login")
+ console.log(" " + CHALK.black.bgWhite("┌─────────────────────────────────────────────────────────────────────────────┐"))
+ console.log(" " + CHALK.black.bgWhite("| Goto " + Address + " to start managing your installation. |"))
+ console.log(" " + CHALK.black.bgWhite("| Default username and password is admin |"))
+ console.log(" " + CHALK.black.bgWhite("└─────────────────────────────────────────────────────────────────────────────┘"))
+}
+
+// Called when bridge is listenting and online
+function getsetupURI(port) {
+ CONFIG.bridgeConfig.QRData = Bridge.getAccessory().setupURI();
+}
+
+// Bridge Pair Change
+function Paired(IsPaired) {
+ UIServer.setBridgePaired(IsPaired);
+}
+
+// Device Change
+function Change(PL, Object, Originator) {
+ if (Object.hasOwnProperty("route") && Object.route.length > 0) {
+ const Payload = {
+ "accessory": Object,
+ "type": "change",
+ "change": PL,
+ "source": Originator
+ }
+
+ if (Routes.hasOwnProperty(Object.route)) {
+ const R = Routes[Object.route];
+ R.process(Payload);
+ }
+
+ }
+}
+
+// Device Pair
+function Pair(paired, Object) {
+ if (Object.hasOwnProperty("route") && Object.route.length > 0) {
+ const Payload = {
+ "accessory": Object,
+ "type": "pair",
+ "isPaired": paired,
+ }
+
+ if (Routes.hasOwnProperty(Object.route)) {
+ const R = Routes[Object.route];
+ R.process(Payload);
+ }
+ }
+}
+
+// Device Identify
+function Identify(paired, Object) {
+ if (Object.hasOwnProperty("route") && Object.route.length > 0) {
+ const Payload = {
+ "accessory": Object,
+ "type": "identify",
+ "isPaired": paired,
+ }
+
+ if (Routes.hasOwnProperty(Object.route)) {
+ const R = Routes[Object.route];
+ R.process(Payload);
+ }
+ }
+}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0c8a977
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Marcus Davies
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8abfce6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,244 @@
+
+
+# Homekit Device Stack
+A Middleware Server, for bringing HomeKit functionality to your Home Automation.
+
+Homekit Device Stack is like no other. it's a NodeJS server with a fully web based front end, that allows you to create fully functional, virtual Homekit Smart accessories, then with those accessories,
+visually wire the events that occur on them into various other automation platforms using common transport mechanisms.
+
+ - HTTP
+ - UDP Broadcast
+ - File
+ - MQTT
+ - Websocket
+ - Custom Route Modules (v4)
+
+ You can for instance, send a command to node-red, openhab & home assistant whenever one of your accessories have been manipulated from your iOS device.
+ infact, as long as the automation platform supports one of the transports above, it will work with Homekit Device Stack, if not, you can write your own Route module.
+
+ ## Custom Route modules
+ As of Version 4, routes are now provided in the form of nodejs modules.
+ Routes are 'plugins', that extend the transport abilities of Homekit Device Stack.
+
+ Whilst Homekit Device Stack has not been written to work with physical devices directly, given enough effort, you could write a route module that does just that.
+
+ Click [here](./RouteModule.md), to learn how to write a module, and install it.
+
+
+ The system is extremely intuitive.
+
+ - Create an accessory
+ - Create a route (the transport method, and endpoint)
+ - Connect the 2 (drag-connect)
+ - Done!
+
+ routes can be used by more than 1 accessory, and you can create any number of routes.
+
+
+
+The accessories are pre-confgiured, and you only need to supply the metadata for them, i.e the accessory name, the inputs available (Smart TV), the camera stream source (Smart Camera), and so on.
+
+currently, 16 different accessory types are supported. - the aim of course is to keep increasing this.
+
+ - CCTV Camera
+ - Door/Window Contact Sensor
+ - Electrical Outlet
+ - Fan
+ - Garage Door Opener
+ - Leak Sensor
+ - Light Sensor
+ - Lock
+ - Motion Sensor
+ - Security System
+ - Smart Light Bulb
+ - Smart TV
+ - Smoke Sensor
+ - Switch
+ - Temperature Sensor
+ - Thermostat
+
+
+The message that is sent using your transport route is below.
+```json
+{
+ "accessory": {
+ "type": "SWITCH",
+ "name": "Switch Accessory Demo",
+ "accessoryID": "CD2947583B71"
+ },
+ "type": "change",
+ "change": {
+ "characteristic": "On",
+ "value": true
+ },
+ "source": "iOS_DEVICE",
+ "route_type": "FILE",
+ "route_name": "File Output"
+}
+```
+
+There are 3 possible event types:
+
+| Event | Description |
+| --------------- | --------------------------------------------------------- |
+| change | One of the characteristics has changed. |
+| identify | The device has been identified (clicking identify in iOS) |
+| pair | The pairing state of the non bridged device has changed. |
+
+The **source** object in the change payload above, identifies where the change occurred **iOS_DEVICE** or **API**
+Note : if the event type is **identify** or **pair** then **source** is omitted.
+
+Events **identify** and **pair** will include **isPaired** - which states whether or not the accessory has currently been enrolled.
+
+## Is this homebridge?
+No, homebridge is targeted towards providing Homekit support for non compatible devices.
+The purpose of HomeKit Device Stack, is to provide a homekit virtualisation platform, that allows hot swopping its outgoing communication.
+
+## So, can I change the status of the accessories, without an iOS device
+Yes!
+HomeKit Device Stack has 2 network based inputs.
+
+ - A REST based HTTP API
+ - MQTT Topic Subscription
+
+Both these inputs allow manipulation of the accessory states. These altered states are then reflected in HomeApp, or any other Homekit based application.
+
+Changes originating from these inputs may cause routes to trigger, making use of the **source** object can be used to filter these out.
+
+The URL for the REST API is **http://IP-ADDRESS:7989/{password}/**
+
+| Method | Address | Description |
+| ------ | ----------------------------- | ------------------------------------------------------- |
+| GET | /accessories | Lists all accessories and there current characteristics |
+| GET | /accessories/accessoryID | Same as above but for the identified accessory |
+| PUT | /accessories/accessoryID | Sets characteristics for the identified accessory |
+
+The body in your HTTP PUT command, should be nothing more than a JSON object representing the characteristics to set
+
+```json
+{
+ "On": false,
+ "OutletInUse": true
+}
+```
+The same format should be used for MQTT messages.
+The topic should end with the accessory ID Ex: **HKDS/IN/HDSH389HJS**.
+You can change the leading topic name in the UI. by default its **HKDS/IN/**.
+
+## Installing
+Ensure you have **NodeJS** and **NPM** installed.
+Then install Homekit Device Stack
+
+ npm install homekit-device-stack
+
+## Running
+Within the directory that HomeKit Device Stack is installed.
+
+ node App.js
+
+If creating an auto start script - ensure the script is set to run under the installed directory
+
+## Command line arguments
+**reset** - Completely Resets HomeKit Device Stack (inits a default configuration)
+**passwd** {desired password} - set the admin password
+**installmodule** {name of module} - install a [custom Route Module](./RouteModule.md)
+
+## Credits
+HomeKit Device Stack is based on the awesome [HAP-NodeJS](https://github.com/homebridge/HAP-NodeJS)
+library, without it, projects like this one are not possible.
+
+## Version History
+
+ - **4.0.0**
+ **BREAKING CHANGE : V4 is not backwards compatible with V3 configurations - Sorry :(**
+
+ - Added 4 new sensor devices (Smoke, Light, Leak & Temperature)
+ - Added A Websocket Route
+ - Optimised Route logic
+ - Bump HAP-NodeJS to 0.9.1
+ - Cleaned hap code to fall in line with hap-nodejs spec.
+ - Restore option added to setup page, removing the need to re-enroll, if re-installing HKDS.
+ - Accessory Characteristics are now written to disc and restored, when terminating/starting HKDS.
+ - Improvements to Read Me.
+ - Small improvements to UI (new background being one).
+ - Migrated UI template engine to handlebars
+ - Added the ability to use either **CIAO** or **BONJOUR-HAP** for the advertiser.
+ - Added the ability to attach to a specific interface.
+ - Routes are now provided as modules - allowing enhanced route development
+
+ - **3.0.3**
+ - Fixed 'Accessory Not Responding' after editing a non bridged device.
+ - Fixed potential crash where a no longer existing route is to trigger.
+ - Added pairing events to out going routes for non-bridged devices
+ - Added pairing pin code to UI for non-bridged devices.
+
+ - **3.0.2**
+ - House keeping
+ - Further Camera improvements
+
+ - **3.0.1**
+ - Added delete option when editing routes
+ - Fixed crash on attempting to update an unknown Device ID
+
+ - **3.0.0**
+ - New User Interface (+ Added route configuration to UI)
+ - Optimisations/Improvements/Bug Fixes to the core code
+ - Optimisations/Improvements/Bug Fixes to the camera implementation
+ - **description** property of accessories no longer in use.
+ - Added Fan Accessory
+ - Enhanced MQTT Route to allow Accessory ID in the topic name
+ - Enhanced FILE Route to allow Accessory ID in the folder path
+ - Enhanced HTTP Route to allow Accessory ID in the URI
+ - Added ability to publish your accessories attached or detached from a HomeKit 'Bridge'
+ - Added the ability to backup/restore your configuration.
+ - Added the (optional) motion sensor and door bell services to camera accessories
+ (to support rich notifications in iOS 13+)
+
+
+ - **2.1.0**
+ - Bump all dependencies to latest stable releases
+ - Migrated Camera object to latest **CameraController** API
+ - Added light bulb device.
+
+
+ - **2.0.1**
+ - Fixed inability to hide TV inputs from view.
+ - Fixed security issue allowing access without logging in.
+ - Fixed potential ffmpeg process freeze.
+ - Fixed disconnected web client exception.
+ - Added Route type icon to accessory panel
+
+ - **2.0.0**
+ **BREAKING CHANGES!**
+
+ - Bump all dependencies to latest stable releases
+ - Relocated HKDS and HomeKit configuration (config now survives updates)
+ Make a copy of your **config.json** file and any files inside the **HomeKit** dir, then...
+ **config.json** should be moved to **%Home%/HomeKitDeviceStack**
+ **HomeKit/*.json** should be moved to **%Home%/HomeKitDeviceStack/HomeKitPersist**
+ - The **directory** value for File routes should now only contain a name of a folder
+ that is relative to **%Home%/HomeKitDeviceStack/**
+
+
+ - **1.1.3**
+ - Update Read Me
+ - **1.1.2**
+ - Improved layout for acessories UI.
+ - Outgoing route performance improvements
+ - Fixed null reference for accessories without a defined route (i.e. camera)
+ - **1.1.1**
+ - Removed unused parameter from Server constructor.
+ - **1.1.0**
+ - Added ability to manipulate devices via MQTT
+ - Improved error handling
+ - Fixed showing loopback address in console.
+ - Switched to using axios for the HTTP route type
+ - Switched to internal NodeJS crypto library
+ - **1.0.1**
+ - Fixed typo in read me
+ - **1.0.0**
+ - Initial Release
+
+
+## To Do
+ - Continue to add more accessory types
\ No newline at end of file
diff --git a/RouteModule.md b/RouteModule.md
new file mode 100644
index 0000000..d0eb3de
--- /dev/null
+++ b/RouteModule.md
@@ -0,0 +1,132 @@
+# Developing a Route Module
+In its basic form, a route module is nothing more than a nodejs module, with an index.js and package.json file.
+like any other module in nodejs - your route can require other modules, just add them as a dependency in your package file
+
+## Lets have a look at the HTTP Post Route
+The **package.json** file is needed by all nodejs modules.
+
+**NOTE:**
+Your module name **MUST** begin with **hkds-route-**, if it is not, it will not get loaded.
+
+```json
+{
+ "name": "hkds-route-http",
+ "description": "The stock Homekit Device Stack HTTP route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ },
+ "license": "MIT",
+ "dependencies": {
+ "axios": "0.21.1"
+ }
+}
+```
+
+And the all important **index.js** file
+
+```javascript
+'use strict'
+const axios = require('axios')
+
+/* UI Params */
+const Params = [
+ {
+ id: "destinationURI",
+ label: "HTTP URI"
+ }
+]
+
+/* Metadata */
+const Name = "HTTP Post Output";
+const Icon = "icon.png";
+
+/* Route Class */
+class HTTPRoute {
+
+ /* Constructor */
+ constructor(route) {
+ this.Route = route
+ }
+}
+
+HTTPRoute.prototype.process = async function (payload) {
+
+ let CFG = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Homekit Device Stack'
+ },
+ method: 'post',
+ url: this.Route.destinationURI.replace('{{accessoryID}}', payload.accessory.accessoryID),
+ data: payload
+ }
+
+ try{
+ let Res = await axios.request(CFG);
+ }
+ catch(err){
+ console.log(" HTTP Route Error: "+err)
+ }
+}
+
+HTTPRoute.prototype.close = function (reason) {
+}
+
+module.exports = {
+ "Route": HTTPRoute,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
+```
+
+Your module file (it doesn't have to be called **index.js**), must export 4 objects.
+
+| Property | What it's for |
+|----------|-------------------------------------------------------------------------------|
+|Route | A pointer to your modules main class. |
+|Inputs | An array of input objects |
+|Name | The name as displayed in the UI |
+|Icon | An icon file, relative to the root of your module. |
+
+Your class (Exported as **Route**) must have a constructor that accepts an object representing the route settings, as confgiured in the UI.
+The class must expose 2 prototype methods: **process** and **close**
+
+| Method | What it's for |
+|-------------------------|------------------------------------------------------------------------------------------------|
+|async process(payload) | This is called when an accessory is sending an event, **payload** will contain the event data |
+|close(reason) | This is called when the route is being destroyed (iether **reconfgiure** or **appclose**) |
+
+The **Inputs** object must be an array of input objects, it allows settings to be passed to the route during its constructor.
+
+```json
+[
+ {
+ "id": "some_internal_identifyer",
+ "label": "A Nice Title For The UI"
+ }
+]
+```
+
+## Installing your route module.
+
+**Manual Install**
+ - Bundle everything up in a folder with a name that matches your module name, and copy this folder to **/%Home%/HKDS/node_modules**
+ - Go into your folder, now in **/%Home%/HKDS/node_modules**, and run ```npm install``` to install any dependencies your route module may need.
+ - Restart HomeKit Device Stack.
+
+**Using NPM**
+If your route module has been published to NPM, you can install it with the ```installmodule``` command
+this will also allow you to install 3rd party route modules. A Restart of HomeKit Device Stack will be required in any case.
+
+```node app.js installmodule {name_of_module}```
+
+Or use NPM directly (you will need to specify a --prefix that points to the root config directory of HKDS)
+
+```npm install {name_of_module} --prefix "/%Home%/HKDS"```
+
+
+
+
diff --git a/Routes/hkds-route-console/icon.png b/Routes/hkds-route-console/icon.png
new file mode 100644
index 0000000..593d6f3
Binary files /dev/null and b/Routes/hkds-route-console/icon.png differ
diff --git a/Routes/hkds-route-console/index.js b/Routes/hkds-route-console/index.js
new file mode 100644
index 0000000..558c05d
--- /dev/null
+++ b/Routes/hkds-route-console/index.js
@@ -0,0 +1,51 @@
+'use strict'
+
+/* Clean Payload */
+const CleanPayload = function (Payload, Type) {
+
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* UI Params */
+const Params = [
+]
+
+/* Metadata */
+const Name = "Console Output";
+const Icon = "icon.png";
+
+/* Route Class */
+class ConsoleClass {
+
+ /* Constructor */
+ constructor(route) {
+ }
+}
+
+ConsoleClass.prototype.process = async function (payload) {
+ payload = CleanPayload(payload, "CONSOLE")
+ console.log(payload)
+}
+
+ConsoleClass.prototype.close = function (reason) {
+}
+
+module.exports = {
+ "Route": ConsoleClass,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-console/package.json b/Routes/hkds-route-console/package.json
new file mode 100644
index 0000000..bc319c3
--- /dev/null
+++ b/Routes/hkds-route-console/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "hkds-route-console",
+ "description": "The stock Homekit Device Stack CONSOLE route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-file/icon.png b/Routes/hkds-route-file/icon.png
new file mode 100644
index 0000000..8620d00
Binary files /dev/null and b/Routes/hkds-route-file/icon.png differ
diff --git a/Routes/hkds-route-file/index.js b/Routes/hkds-route-file/index.js
new file mode 100644
index 0000000..ffb25dc
--- /dev/null
+++ b/Routes/hkds-route-file/index.js
@@ -0,0 +1,84 @@
+'use strict'
+const path = require("path");
+const fs = require("fs");
+
+/* Clean Payload */
+const CleanPayload = function (Payload, Type) {
+
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* UI Params */
+const Params = [
+ {
+ id: "directory",
+ label: "Storage Location/Directoy"
+ }
+]
+
+/* Metadata */
+const Name = "File Output";
+const Icon = "icon.png";
+
+/* Route Class */
+class File {
+
+ /* Constructor */
+ constructor(route) {
+ this.Route = route;
+ }
+
+}
+
+File.prototype.process = async function (payload) {
+
+ payload = CleanPayload(payload, "FILE")
+ let JSONs = JSON.stringify(payload);
+
+ let Directory = this.Route.directory.replace("{{accessoryID}}", payload.accessory.accessoryID)
+
+ if (!fs.existsSync(Directory)) {
+ try {
+ fs.mkdirSync(Directory, { recursive: true });
+ }
+ catch (err) {
+ console.log(" FILE Route error: "+err)
+ return;
+ }
+ }
+
+ let DT = new Date().getTime();
+ let FileName = DT + '_' + payload.accessory.accessoryID + ".json"
+
+ let _Path = path.join(Directory, FileName);
+
+ try {
+ fs.writeFileSync(_Path, JSONs, 'utf8')
+ }
+ catch (err) {
+ console.log(" FILE Route error: "+err)
+ }
+}
+
+File.prototype.close = function (reason) {
+}
+
+module.exports = {
+ "Route": File,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-file/package.json b/Routes/hkds-route-file/package.json
new file mode 100644
index 0000000..8a9b4bd
--- /dev/null
+++ b/Routes/hkds-route-file/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "hkds-route-file",
+ "description": "The stock Homekit Device Stack FILE route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT"
+}
diff --git a/Routes/hkds-route-http/icon.png b/Routes/hkds-route-http/icon.png
new file mode 100644
index 0000000..97f08f1
Binary files /dev/null and b/Routes/hkds-route-http/icon.png differ
diff --git a/Routes/hkds-route-http/index.js b/Routes/hkds-route-http/index.js
new file mode 100644
index 0000000..0f34a8b
--- /dev/null
+++ b/Routes/hkds-route-http/index.js
@@ -0,0 +1,75 @@
+'use strict'
+const axios = require('axios')
+
+/* Clean Payload */
+const CleanPayload = function (Payload, Type) {
+
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* UI Params */
+const Params = [
+ {
+ id: "destinationURI",
+ label: "HTTP URI"
+ }
+]
+
+/* Metadata */
+const Name = "HTTP POST Output";
+const Icon = "icon.png";
+
+/* Route Class */
+class HTTPRoute {
+
+ /* Constructor */
+ constructor(route) {
+ this.Route = route
+ }
+}
+
+HTTPRoute.prototype.process = async function (payload) {
+
+ payload = CleanPayload(payload, "HTTP")
+
+ let CFG = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Homekit Device Stack'
+ },
+ method: 'post',
+ url: this.Route.destinationURI.replace('{{accessoryID}}', payload.accessory.accessoryID),
+ data: payload
+ }
+
+ try{
+ let Res = await axios.request(CFG)
+ }
+ catch(err){
+ console.log(" HTTP Route error: "+err)
+ }
+
+}
+
+HTTPRoute.prototype.close = function (reason) {
+}
+
+module.exports = {
+ "Route": HTTPRoute,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-http/package.json b/Routes/hkds-route-http/package.json
new file mode 100644
index 0000000..4ac2c23
--- /dev/null
+++ b/Routes/hkds-route-http/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "hkds-route-http",
+ "description": "The stock Homekit Device Stack HTTP route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "axios": "0.21.1"
+ }
+}
diff --git a/Routes/hkds-route-mqtt/icon.png b/Routes/hkds-route-mqtt/icon.png
new file mode 100644
index 0000000..0269f28
Binary files /dev/null and b/Routes/hkds-route-mqtt/icon.png differ
diff --git a/Routes/hkds-route-mqtt/index.js b/Routes/hkds-route-mqtt/index.js
new file mode 100644
index 0000000..6dcdeab
--- /dev/null
+++ b/Routes/hkds-route-mqtt/index.js
@@ -0,0 +1,92 @@
+'use strict'
+const mqtt = require('mqtt')
+
+/* Clean Payload */
+const CleanPayload = function (Payload, Type) {
+
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* UI Params */
+const Params = [
+ {
+ id: "mqttbroker",
+ label: "MQTT Broker"
+ },
+ {
+ id: "mqttusername",
+ label: "Username"
+ },
+ {
+ id: "mqttpassword",
+ label: "Password"
+ },
+ {
+ id: "mqtttopic",
+ label: "Topic"
+ }
+]
+
+/* Metadata */
+const Name = "MQTT Message";
+const Icon = "icon.png";
+
+/* Route Class */
+class MQTTRoute {
+
+ /* Constructor */
+ constructor(route) {
+
+ this.Route = route;
+
+ let Options = {
+ username: route.mqttusername,
+ password: route.mqttpassword
+ }
+
+ this.MQTTBroker = mqtt.connect(route.mqttbroker, Options)
+ this.MQTTBroker.on('connect', () => this.mqttConnected())
+ this.MQTTBroker.on('error', (e) => this.mqttError(e))
+ }
+}
+
+MQTTRoute.prototype.process = async function (payload) {
+
+ payload = CleanPayload(payload, "MQTT")
+ let JSONs = JSON.stringify(payload);
+
+ let T = this.Route.mqtttopic.replace("{{accessoryID}}", payload.accessory.accessoryID)
+ this.MQTTBroker.publish(T, JSONs, null, () => { });
+}
+
+MQTTRoute.prototype.close = function (reason) {
+ this.MQTTBroker.end();
+}
+
+MQTTRoute.prototype.mqttConnected = function () {
+ console.log(" MQTT Route ready.");
+}
+
+MQTTRoute.prototype.mqttError = function (err) {
+ console.log(" MQTT Route error: " + err);
+}
+
+module.exports = {
+ "Route": MQTTRoute,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-mqtt/package.json b/Routes/hkds-route-mqtt/package.json
new file mode 100644
index 0000000..1867d47
--- /dev/null
+++ b/Routes/hkds-route-mqtt/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "hkds-route-mqtt",
+ "description": "The stock Homekit Device Stack MQTT route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "mqtt": "4.2.6"
+ }
+}
diff --git a/Routes/hkds-route-udp/icon.png b/Routes/hkds-route-udp/icon.png
new file mode 100644
index 0000000..58107b3
Binary files /dev/null and b/Routes/hkds-route-udp/icon.png differ
diff --git a/Routes/hkds-route-udp/index.js b/Routes/hkds-route-udp/index.js
new file mode 100644
index 0000000..14d9158
--- /dev/null
+++ b/Routes/hkds-route-udp/index.js
@@ -0,0 +1,79 @@
+'use strict'
+const dgram = require("dgram");
+
+/* Clean Payload */
+const CleanPayload = function (Payload, Type) {
+
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* UI Params */
+const Params = [
+ {
+ id: "address",
+ label: "Broadcast Address"
+ },
+ {
+ id: "port",
+ label: "Broadcast Port"
+ }
+]
+
+/* Metadata */
+const Name = "UDP Broadcast";
+const Icon = "icon.png";
+
+/* Route Class */
+class UDP {
+
+ /* Constructor */
+ constructor(route) {
+
+ this.Route = route;
+
+ this.UDPServer = dgram.createSocket("udp4");
+ this.UDPServer.bind(() => this.UDPConnected())
+ }
+}
+
+UDP.prototype.process = async function (payload) {
+
+ payload = CleanPayload(payload, "UDP")
+ let JSONs = JSON.stringify(payload);
+ this.UDPServer.send(JSONs, 0, JSONs.length, this.Route.port, this.Route.address, this.UDPDone);
+}
+
+UDP.prototype.close = function (reason) {
+ this.UDPServer.close();
+}
+
+UDP.prototype.UDPConnected = function () {
+ this.UDPServer.setBroadcast(true);
+ console.log(" UDP Route ready.");
+}
+
+UDP.prototype.UDPDone = function (e, n) {
+ if (e) {
+ console.log(" UDP Route error: " + e);
+ }
+}
+
+module.exports = {
+ "Route": UDP,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-udp/package.json b/Routes/hkds-route-udp/package.json
new file mode 100644
index 0000000..cd8ea4d
--- /dev/null
+++ b/Routes/hkds-route-udp/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "hkds-route-udp",
+ "description": "The stock Homekit Device Stack UDP route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT"
+}
diff --git a/Routes/hkds-route-websocket/icon.png b/Routes/hkds-route-websocket/icon.png
new file mode 100644
index 0000000..cb64ce5
Binary files /dev/null and b/Routes/hkds-route-websocket/icon.png differ
diff --git a/Routes/hkds-route-websocket/index.js b/Routes/hkds-route-websocket/index.js
new file mode 100644
index 0000000..18b48f1
--- /dev/null
+++ b/Routes/hkds-route-websocket/index.js
@@ -0,0 +1,78 @@
+'use strict'
+const WS = require("ws");
+
+/* Clean Payload */
+const CleanPayload = function (Payload, Type) {
+
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* UI Params */
+const Params = [
+ {
+ id: "uri",
+ label: "Websocket Address"
+ }
+]
+
+/* Metadata */
+const Name = "Websocket";
+const Icon = "icon.png";
+
+
+/* Route Class */
+class WebsocketClass {
+
+ /* Constructor */
+ constructor(route) {
+
+ this.Websocket = new WS(route.uri);
+
+ this.Websocket.on('open', () => this.HandleWSOpen());
+ this.Websocket.on('error', (e) => this.WSError(e))
+
+ }
+}
+
+
+WebsocketClass.prototype.process = async function (payload) {
+
+ payload = CleanPayload(payload, "WEBSOCKET")
+ let JSONs = JSON.stringify(payload);
+
+ this.Websocket.send(JSONs);
+
+}
+
+WebsocketClass.prototype.close = function () {
+ this.Websocket.close();
+}
+
+
+WebsocketClass.prototype.HandleWSOpen = function () {
+ console.log(" WEBSOCKET Route ready.");
+}
+
+WebsocketClass.prototype.WSError = function (err) {
+ console.log(" WEBSOCKET Route error: " + err);
+}
+
+module.exports = {
+ "Route": WebsocketClass,
+ "Inputs": Params,
+ "Name": Name,
+ "Icon": Icon
+}
\ No newline at end of file
diff --git a/Routes/hkds-route-websocket/package.json b/Routes/hkds-route-websocket/package.json
new file mode 100644
index 0000000..a70abcb
--- /dev/null
+++ b/Routes/hkds-route-websocket/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "hkds-route-websocket",
+ "description": "The stock Homekit Device Stack WEB SOCKET route",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "ws":"7.4.3"
+ }
+}
diff --git a/core/accessory.js b/core/accessory.js
new file mode 100755
index 0000000..4803c2d
--- /dev/null
+++ b/core/accessory.js
@@ -0,0 +1,1287 @@
+'use strict'
+const HapNodeJS = require("hap-nodejs");
+const Service = HapNodeJS.Service;
+const Accessory = HapNodeJS.Accessory;
+const Characteristic = HapNodeJS.Characteristic;
+const uuid = HapNodeJS.uuid;
+const CharacteristicEventTypes = HapNodeJS.CharacteristicEventTypes;
+const AccessoryEventTypes = HapNodeJS.AccessoryEventTypes;
+const EventEmitter = require("events");
+const PKG = require('../package.json');
+const CameraSource = require('./cameraSource');
+const Util = require('./util');
+const CameraController = HapNodeJS.CameraController;
+const Catagories = HapNodeJS.Categories;
+const BridgeCLS = HapNodeJS.Bridge;
+const config = require(Util.ConfigPath);
+
+let Initialised = false;
+
+/**
+ * Common Accessory Class
+ * A prototype class from which all accessories are based.
+ * It contains the event emitter, the creation of the accessory its self, and attaching some needed events.
+ */
+class AccessoryCLS extends EventEmitter {
+ constructor(AccessoryOBJ, Category) {
+ super();
+
+ this._Config = AccessoryOBJ;
+ this._Properties = {};
+ this._isBridge = (Category == Catagories.BRIDGE);
+
+ this._Properties = {};
+
+ if (!Initialised) {
+ HapNodeJS.HAPStorage.setCustomStoragePath(Util.HomeKitPath);
+ Initialised = true;
+ }
+
+ const UUID = uuid.generate('hap-nodejs:accessories:' + AccessoryOBJ.name + ':' + AccessoryOBJ.username);
+
+ if (this._isBridge) {
+ this._accessory = new BridgeCLS(AccessoryOBJ.name, UUID);
+ } else {
+ this._accessory = new Accessory(AccessoryOBJ.name, UUID);
+ }
+
+ this._accessory.getService(Service.AccessoryInformation)
+ .setCharacteristic(Characteristic.SerialNumber, AccessoryOBJ.serialNumber)
+ .setCharacteristic(Characteristic.Manufacturer, "Marcus Davies")
+ .setCharacteristic(Characteristic.FirmwareRevision, PKG.version)
+ .setCharacteristic(Characteristic.Name, AccessoryOBJ.name)
+
+ this._accessory.username = AccessoryOBJ.username;
+ this._accessory.pincode = AccessoryOBJ.pincode;
+ this._accessory.category = Category;
+ this._accessory.setupID = AccessoryOBJ.setupID;
+
+ this._accessory.on(AccessoryEventTypes.IDENTIFY, (paired, callback) => {
+ callback();
+ this.emit("IDENTIFY", paired)
+ });
+
+ this._accessory.on(AccessoryEventTypes.LISTENING, (port) => {
+ this.emit("LISTENING", port)
+ });
+
+ this._accessory.on(AccessoryEventTypes.PAIRED, () => {
+ this.emit("PAIR_CHANGE", true);
+ });
+
+ this._accessory.on(AccessoryEventTypes.UNPAIRED, () => {
+ this.emit("PAIR_CHANGE", false);
+ });
+ }
+}
+
+/**
+ * Helper method to attach get and set routines
+ */
+AccessoryCLS.prototype._wireUpEvents = function(targetService, EventStruct) {
+ const GetHooks = EventStruct.Get;
+ const SetHooks = EventStruct.Set;
+
+ for (let i = 0; i < GetHooks.length; i++) {
+ targetService.getCharacteristic(Characteristic[GetHooks[i]])
+ .on(CharacteristicEventTypes.GET, (cb) => this._get(GetHooks[i], cb))
+ }
+
+ for (let i = 0; i < SetHooks.length; i++) {
+ targetService.getCharacteristic(Characteristic[SetHooks[i]])
+ .on(CharacteristicEventTypes.SET, (value, callback, ctx, connection) => this._set(SetHooks[i], value, callback, connection))
+ }
+}
+
+/**
+ * Internal set
+ */
+AccessoryCLS.prototype._set = function(property, value, callback, connection) {
+ this._Properties[property] = value;
+ callback(null);
+
+ const PL = {
+ "characteristic": property,
+ "value": value,
+ }
+
+ this.emit("STATE_CHANGE", PL, connection == null ? "API" : "iOS_DEVICE");
+
+}
+/**
+ * Internal get
+ */
+AccessoryCLS.prototype._get = function(property, callback) {
+ if (this._Properties[property] != null) {
+ callback(null, this._Properties[property]);
+ } else {
+ callback(null, null);
+ }
+}
+/**
+ * Get Accessory
+ */
+AccessoryCLS.prototype.getAccessory = function() {
+ return this._accessory;
+}
+
+/**
+ * Get Type
+ */
+AccessoryCLS.prototype.getAccessoryType = function() {
+ return this._Config.type;
+}
+/**
+ * Publish
+ */
+AccessoryCLS.prototype.publish = function() {
+
+ let CFG = {
+ username: this._accessory.username,
+ pincode: this._accessory.pincode,
+ category: this._accessory.category,
+ setupID: this._accessory.setupID,
+ advertiser: config.advertiser
+ }
+
+ if (config.interface != 'ALL') {
+ CFG.bind = config.interface
+ }
+
+ this._accessory.publish(CFG)
+}
+/**
+ * unpublish
+ */
+AccessoryCLS.prototype.unpublish = function(destroy) {
+ if (destroy) {
+ this._accessory.destroy();
+ } else {
+ this._accessory.unpublish()
+ }
+}
+/**
+ * get all properties
+ */
+AccessoryCLS.prototype.getProperties = function() {
+ return this._Properties;
+
+}
+/**
+ * add accessory (for bridge)
+ */
+AccessoryCLS.prototype.addAccessory = function(Accessory) {
+ if (this._isBridge) {
+ this._accessory.addBridgedAccessory(Accessory);
+
+ }
+}
+/**
+ * remove accessory (for bridge)
+ */
+AccessoryCLS.prototype.removeAccessory = function(Accessory) {
+ if (this._isBridge) {
+ this._accessory.removeBridgedAccessory(Accessory, false)
+
+ }
+}
+/**
+ * get accessories (for bridge)
+ */
+AccessoryCLS.prototype.getAccessories = function() {
+ if (this._isBridge) {
+ return this._accessory.bridgedAccessories;
+
+ }
+}
+/**
+ * helper method to create a battery service
+ */
+AccessoryCLS.prototype._createBatteryService = function() {
+ this._batteryService = new Service.BatteryService('', '');
+ this._batteryService.setCharacteristic(Characteristic.BatteryLevel, 100);
+ this._batteryService.setCharacteristic(Characteristic.StatusLowBattery, 0);
+ this._batteryService.setCharacteristic(Characteristic.ChargingState, 0);
+ this._Properties["BatteryLevel"] = 100;
+ this._Properties["StatusLowBattery"] = 0;
+ this._Properties["ChargingState"] = 0;
+
+ const EventStruct = {
+ "Get": ["BatteryLevel", "StatusLowBattery", "ChargingState"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._batteryService, EventStruct)
+ this._accessory.addService(this._batteryService);
+}
+
+/**
+ * Main Bridge
+ */
+class Bridge extends AccessoryCLS {
+ constructor(Config) {
+ Config.name = "HomeKit Device Stack"
+ super(Config, Catagories.BRIDGE);
+ this._accessory.getService(Service.AccessoryInformation)
+ .setCharacteristic(Characteristic.Model, "HKDS4")
+
+ }
+}
+
+/**
+ * Public Basic Set
+ */
+const _basicSet = function(payload) {
+ const Props = Object.keys(payload);
+
+ for (let i = 0; i < Props.length; i++) {
+ this._Properties[Props[i]] = payload[Props[i]];
+ this._service.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+
+ }
+}
+/**
+ * Public Set with a possible battey service
+ */
+const _setWithBattery = function(payload) {
+ const Props = Object.keys(payload);
+ const BatteryTargets = ["BatteryLevel", "StatusLowBattery", "ChargingState"]
+
+ for (let i = 0; i < Props.length; i++) {
+ this._Properties[Props[i]] = payload[Props[i]];
+
+ if (BatteryTargets.includes(Props[i])) {
+ this._batteryService.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+ } else {
+ this._service.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+ }
+
+ }
+}
+
+/**
+ * Outlet Accessory
+ */
+class Outlet extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.OUTLET);
+
+ this._service = new Service.Outlet(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.On, false);
+ this._service.setCharacteristic(Characteristic.OutletInUse, false);
+ this._Properties["On"] = false;
+ this._Properties["OutletInUse"] = false;
+
+ const EventStruct = {
+ "Get": ["On", "OutletInUse"],
+ "Set": ["On"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+ }
+}
+Outlet.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Fan Accessory
+ */
+class Fan extends AccessoryCLS {
+ constructor(Config) {
+ super(Config, Catagories.FAN);
+
+ this._service = new Service.Fan(Config.name, Config.name)
+
+ this._service.setCharacteristic(Characteristic.On, false);
+ this._Properties["On"] = false;
+ this._service.setCharacteristic(Characteristic.RotationSpeed, 100);
+ this._Properties["RotationSpeed"] = 100;
+
+ const EventStruct = {
+ "Get": ["On", "RotationSpeed"],
+ "Set": ["On", "RotationSpeed"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ }
+}
+Fan.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Switch Accessory
+ */
+class Switch extends AccessoryCLS {
+ constructor(Config) {
+ super(Config, Catagories.SWITCH);
+
+ this._service = new Service.Switch(Config.name, Config.name)
+
+ this._service.setCharacteristic(Characteristic.On, false);
+ this._Properties["On"] = false;
+
+ const EventStruct = {
+ "Get": ["On"],
+ "Set": ["On"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ }
+}
+Switch.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Alarm Accessory
+ */
+class Alarm extends AccessoryCLS {
+ constructor(Config) {
+ super(Config, Catagories.SECURITY_SYSTEM);
+
+ this._service = new Service.SecuritySystem(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._service.setCharacteristic(Characteristic.SecuritySystemCurrentState, 3);
+ this._service.setCharacteristic(Characteristic.SecuritySystemTargetState, 3);
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+ this._Properties["SecuritySystemCurrentState"] = 3;
+ this._Properties["SecuritySystemTargetState"] = 3;
+
+ const EventStruct = {
+ "Get": ["SecuritySystemTargetState", "StatusFault", "StatusTampered", "SecuritySystemCurrentState"],
+ "Set": ["SecuritySystemTargetState"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ }
+}
+Alarm.prototype.setCharacteristics = _basicSet;
+
+/**
+ * TV Accessory Speaker Set support
+ */
+const _TVSet = function(payload) {
+ const Props = Object.keys(payload);
+
+ for (let i = 0; i < Props.length; i++) {
+ this._Properties[Props[i]] = payload[Props[i]];
+
+ this._service.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+ if (Props[i] == "Active") {
+ // speaker and tv are one
+ this._Speaker.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+ }
+
+ }
+}
+/**
+ * TV Accessory
+ */
+class TV extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.TELEVISION);
+
+ this._Inputs = [];
+
+ this._service = new Service.Television(Config.name, Config.Name);
+ this._service.setCharacteristic(Characteristic.ConfiguredName, Config.name);
+ this._service.setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE);
+ this._service.setCharacteristic(Characteristic.ActiveIdentifier, 1);
+ this._service.setCharacteristic(Characteristic.Active, 0);
+ this._Properties["Active"] = 0;
+ this._Properties["ActiveIdentifier"] = 1;
+
+ var EventStruct = {
+ "Get": ["Active", "ActiveIdentifier"],
+ "Set": ["Active", "RemoteKey", "ActiveIdentifier", "PowerModeSelection"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ // Speaker
+ this._Speaker = new Service.TelevisionSpeaker('', '')
+ this._Speaker.setCharacteristic(Characteristic.Active, 0)
+ this._Speaker.setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE);
+
+ EventStruct = {
+ "Get": ["Active", "VolumeSelector"],
+ "Set": ["VolumeSelector"]
+ }
+
+ this._wireUpEvents(this._Speaker, EventStruct);
+ this._accessory.addService(this._Speaker);
+
+ // Inputs
+ for (let i = 0; i < Config.inputs.length; i++) {
+ if (Config.inputs[i].length < 1) {
+ continue;
+ }
+ const Input = new Service.InputSource(Config.inputs[i], Config.inputs[i])
+ Input.setCharacteristic(Characteristic.Identifier, (i + 1))
+ Input.setCharacteristic(Characteristic.ConfiguredName, Config.inputs[i])
+ Input.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED)
+ Input.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI)
+ Input.setCharacteristic(Characteristic.CurrentVisibilityState, 0);
+ Input.setCharacteristic(Characteristic.TargetVisibilityState, 0);
+
+ Input.getCharacteristic(Characteristic.TargetVisibilityState)
+ .on(CharacteristicEventTypes.SET, function(value, callback, hap) {
+ Input.setCharacteristic(Characteristic.CurrentVisibilityState, value);
+ callback(null);
+ })
+
+ this._accessory.addService(Input);
+ this._service.addLinkedService(Input);
+
+ this._Inputs.push(this.Input);
+ }
+
+ }
+}
+TV.prototype.setCharacteristics = _TVSet;
+
+/**
+ * CCTV Specific Sets
+ */
+const _CCTVSet = function(payload) {
+ const Props = Object.keys(payload);
+
+ const DoorBellTargets = ["ProgrammableSwitchEvent"]
+ const MotionTargets = ["MotionDetected", "StatusActive", "StatusFault", "StatusTampered"]
+
+ for (let i = 0; i < Props.length; i++) {
+ this._Properties[Props[i]] = payload[Props[i]];
+
+ if (DoorBellTargets.includes(Props[i])) {
+ this._VDBService.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+ } else if (MotionTargets.includes(Props[i])) {
+ this._MDService.setCharacteristic(Characteristic[Props[i]], payload[Props[i]])
+ }
+
+ }
+}
+/**
+ * CCTV Camera
+ */
+class Camera extends AccessoryCLS {
+
+ constructor(Config) {
+ // Door Bell?
+ if (Config.enableDoorbellService == 'true') {
+ super(Config, Catagories.VIDEO_DOORBELL);
+
+ this._VDBService = new Service.Doorbell('', '');
+ this._VDBService.setCharacteristic(Characteristic.ProgrammableSwitchEvent, null);
+ this._Properties["ProgrammableSwitchEvent"] = null;
+
+ const _VDBService_ES = {
+ "Get": ["ProgrammableSwitchEvent"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._VDBService, _VDBService_ES);
+ this._accessory.addService(this._VDBService);
+ } else {
+ super(Config, Catagories.IP_CAMERA);
+ }
+
+ // Camera
+ const Options = {
+ supportedCryptoSuites: [HapNodeJS.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80],
+ video: {
+ codec: {
+ profiles: [HapNodeJS.H264Profile.BASELINE, HapNodeJS.H264Profile.MAIN, HapNodeJS.H264Profile.HIGH],
+ levels: [HapNodeJS.H264Level.LEVEL3_1, HapNodeJS.H264Level.LEVEL3_2, HapNodeJS.H264Level.LEVEL4_0],
+ }
+ }
+ }
+
+ const videoResolutions = []
+
+ this.maxFPS = Config.maxFPS > 30 ? 30 : Config.maxFPS
+ this.maxWidth = Config.maxWidthHeight.split("x")[0];
+ this.maxHeight = Config.maxWidthHeight.split("x")[1];
+
+ if (this.maxWidth >= 320) {
+ if (this.maxHeight >= 240) {
+ videoResolutions.push([320, 240, this.maxFPS])
+ if (this.maxFPS > 15) {
+ videoResolutions.push([320, 240, 15])
+ }
+ }
+ if (this.maxHeight >= 180) {
+ videoResolutions.push([320, 180, this.maxFPS])
+ if (this.maxFPS > 15) {
+ videoResolutions.push([320, 180, 15])
+ }
+ }
+ }
+ if (this.maxWidth >= 480) {
+ if (this.maxHeight >= 360) {
+ videoResolutions.push([480, 360, this.maxFPS])
+ }
+ if (this.maxHeight >= 270) {
+ videoResolutions.push([480, 270, this.maxFPS])
+ }
+ }
+ if (this.maxWidth >= 640) {
+ if (this.maxHeight >= 480) {
+ videoResolutions.push([640, 480, this.maxFPS])
+ }
+ if (this.maxHeight >= 360) {
+ videoResolutions.push([640, 360, this.maxFPS])
+ }
+ }
+ if (this.maxWidth >= 1280) {
+ if (this.maxHeight >= 960) {
+ videoResolutions.push([1280, 960, this.maxFPS])
+ }
+ if (this.maxHeight >= 720) {
+ videoResolutions.push([1280, 720, this.maxFPS])
+ }
+ }
+ if (this.maxWidth >= 1920) {
+ if (this.maxHeight >= 1080) {
+ videoResolutions.push([1920, 1080, this.maxFPS])
+ }
+ }
+
+ Options.video.resolutions = videoResolutions;
+
+ if (Config.enableAudio == 'true') {
+ Options.audio = {
+ codecs: [{
+ type: HapNodeJS.AudioStreamingCodecType.AAC_ELD,
+ samplerate: HapNodeJS.AudioStreamingSamplerate.KHZ_16,
+ audioChannels: 1,
+ bitrate: HapNodeJS.AudioBitrate.VARIABLE
+ }]
+ }
+ }
+
+ this.CameraDelegate = new CameraSource.Camera(Config)
+ this.Controller = new CameraController({
+ cameraStreamCount: Config.maxStreams,
+ delegate: this.CameraDelegate,
+ streamingOptions: Options
+ });
+
+ this.CameraDelegate.attachController(this.Controller);
+ this._accessory.configureController(this.Controller);
+
+ // Motion?
+ if (Config.enableMotionDetectionService == 'true') {
+ this._MDService = new Service.MotionSensor('', '');
+ this._MDService.setCharacteristic(Characteristic.MotionDetected, false);
+ this._MDService.setCharacteristic(Characteristic.StatusActive, 1);
+ this._MDService.setCharacteristic(Characteristic.StatusFault, 0);
+ this._MDService.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._Properties["MotionDetected"] = false;
+ this._Properties["StatusActive"] = 1;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+
+ const _MDService_ES = {
+ "Get": ["MotionDetected", "StatusActive", "StatusTampered", "StatusFault"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._MDService, _MDService_ES);
+ this._accessory.addService(this._MDService);
+ }
+
+ }
+}
+Camera.prototype.setCharacteristics = _CCTVSet;
+
+/**
+ * Contact Accessory
+ */
+class Contact extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.SENSOR);
+
+ this._service = new Service.ContactSensor(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.ContactSensorState, 0);
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._service.setCharacteristic(Characteristic.StatusActive, 1);
+ this._Properties["ContactSensorState"] = 0;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+ this._Properties["StatusActive"] = 1;
+
+ const EventStruct = {
+ "Get": ["ContactSensorState", "StatusFault", "StatusTampered", "StatusActive"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ this._createBatteryService();
+ }
+}
+Contact.prototype.setCharacteristics = _setWithBattery;
+
+/**
+ * Motion Sensor Accessory
+ */
+class Motion extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.SENSOR);
+
+ this._service = new Service.MotionSensor(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.MotionDetected, false);
+ this._service.setCharacteristic(Characteristic.StatusActive, 1);
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._Properties["MotionDetected"] = false;
+ this._Properties["StatusActive"] = 1;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+
+ const EventStruct = {
+ "Get": ["MotionDetected", "StatusActive", "StatusTampered", "StatusFault"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ this._createBatteryService();
+ }
+}
+Motion.prototype.setCharacteristics = _setWithBattery;
+
+/**
+ * Lock Accessory
+ */
+class Lock extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.DOOR_LOCK);
+
+ this._service = new Service.LockMechanism(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.LockTargetState, 0);
+ this._service.setCharacteristic(Characteristic.LockCurrentState, 0);
+ this._Properties["LockTargetState"] = 0;
+ this._Properties["LockCurrentState"] = 0;
+
+ const EventStruct = {
+ "Get": ["LockTargetState", "LockCurrentState"],
+ "Set": ["LockTargetState"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+ }
+}
+Lock.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Light Accessory
+ */
+class LightBulb extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.LIGHTBULB);
+
+ this._service = new Service.Lightbulb(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.On, false);
+ this._Properties["On"] = false;
+
+ const EventStruct = {
+ "Get": ["On"],
+ "Set": ["On"]
+ }
+
+ if (Config.supportsBrightness == 'true') {
+ this._service.setCharacteristic(Characteristic.Brightness, 100);
+ this._Properties["Brightness"] = 100;
+ EventStruct.Get.push("Brightness")
+ EventStruct.Set.push("Brightness")
+ }
+
+ switch (Config.colorMode) {
+ case "hue":
+ this._service.setCharacteristic(Characteristic.Hue, 0);
+ this._Properties["Hue"] = 0;
+ EventStruct.Get.push("Hue")
+ EventStruct.Set.push("Hue")
+
+ this._service.setCharacteristic(Characteristic.Saturation, 0);
+ this._Properties["Saturation"] = 0;
+ EventStruct.Get.push("Saturation")
+ EventStruct.Set.push("Saturation")
+
+ break;
+
+ case "temperature":
+ this._service.setCharacteristic(Characteristic.ColorTemperature, 50);
+ this._Properties["ColorTemperature"] = 50;
+ EventStruct.Get.push("ColorTemperature")
+ EventStruct.Set.push("ColorTemperature")
+
+ break;
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+ }
+}
+LightBulb.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Garage Door Accessory
+ */
+class GarageDoor extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.GARAGE_DOOR_OPENER);
+
+ this._service = new Service.GarageDoorOpener(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.CurrentDoorState, 0);
+ this._service.setCharacteristic(Characteristic.TargetDoorState, 0);
+ this._service.setCharacteristic(Characteristic.LockCurrentState, 0);
+ this._service.setCharacteristic(Characteristic.LockTargetState, 0);
+ this._service.setCharacteristic(Characteristic.ObstructionDetected, false);
+ this._Properties["CurrentDoorState"] = 0;
+ this._Properties["TargetDoorState"] = 0;
+ this._Properties["LockCurrentState"] = 0;
+ this._Properties["LockTargetState"] = 0;
+ this._Properties["ObstructionDetected"] = false;
+
+ const EventStruct = {
+ "Get": ["CurrentDoorState", "TargetDoorState", "LockCurrentState", "LockTargetState", "ObstructionDetected"],
+ "Set": ["TargetDoorState", "LockTargetState"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+ }
+}
+GarageDoor.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Thermotsat Accessory
+ */
+class Thermostat extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.THERMOSTAT);
+
+ this._service = new Service.Thermostat(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.CurrentHeatingCoolingState, 0);
+ this._service.setCharacteristic(Characteristic.TargetHeatingCoolingState, 0);
+ this._service.setCharacteristic(Characteristic.CurrentTemperature, 21);
+ this._service.setCharacteristic(Characteristic.TargetTemperature, 21);
+ this._service.setCharacteristic(Characteristic.TemperatureDisplayUnits, 0);
+ this._service.setCharacteristic(Characteristic.CoolingThresholdTemperature, 26);
+ this._service.setCharacteristic(Characteristic.HeatingThresholdTemperature, 18);
+
+ this._Properties["CurrentHeatingCoolingState"] = 0;
+ this._Properties["TargetHeatingCoolingState"] = 0;
+ this._Properties["CurrentTemperature"] = 21;
+ this._Properties["TargetTemperature"] = 21;
+ this._Properties["TemperatureDisplayUnits"] = 0;
+ this._Properties["CoolingThresholdTemperature"] = 26;
+ this._Properties["HeatingThresholdTemperature"] = 18;
+
+ const EventStruct = {
+ "Get": ["TargetHeatingCoolingState", "CurrentHeatingCoolingState", "TemperatureDisplayUnits", "CurrentTemperature", "TargetTemperature", "CoolingThresholdTemperature", "HeatingThresholdTemperature"],
+ "Set": ["TargetHeatingCoolingState", "TemperatureDisplayUnits", "TargetTemperature", "CoolingThresholdTemperature", "HeatingThresholdTemperature"]
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+ }
+}
+Thermostat.prototype.setCharacteristics = _basicSet;
+
+/**
+ * Temperature Sensor Accessory
+ */
+class Temperature extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.SENSOR);
+
+ this._service = new Service.TemperatureSensor(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.CurrentTemperature, 21);
+ this._service.setCharacteristic(Characteristic.StatusActive, 1);
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._Properties["CurrentTemperature"] = 21;
+ this._Properties["StatusActive"] = 1;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+
+ const EventStruct = {
+ "Get": ["CurrentTemperature", "StatusActive", "StatusTampered", "StatusFault"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ this._createBatteryService();
+ }
+}
+Temperature.prototype.setCharacteristics = _setWithBattery;
+
+/**
+ * Smoke Sensor Accessory
+ */
+class Smoke extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.SENSOR);
+
+ this._service = new Service.SmokeSensor(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.SmokeDetected, 0);
+ this._service.setCharacteristic(Characteristic.StatusActive, 1);
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._Properties["SmokeDetected"] = 0;
+ this._Properties["StatusActive"] = 1;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+
+ const EventStruct = {
+ "Get": ["SmokeDetected", "StatusActive", "StatusTampered", "StatusFault"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ this._createBatteryService();
+ }
+}
+Smoke.prototype.setCharacteristics = _setWithBattery;
+
+/**
+ * Leak Sensor Accessory
+ */
+class Leak extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.SENSOR);
+
+ this._service = new Service.LeakSensor(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.LeakDetected, 0);
+ this._service.setCharacteristic(Characteristic.StatusActive, 1);
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._Properties["LeakDetected"] = 0;
+ this._Properties["StatusActive"] = 1;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+
+ const EventStruct = {
+ "Get": ["LeakDetected", "StatusActive", "StatusTampered", "StatusFault"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ this._createBatteryService();
+ }
+}
+Leak.prototype.setCharacteristics = _setWithBattery;
+
+/**
+ * Light Sensor Accessory
+ */
+class LightSensor extends AccessoryCLS {
+
+ constructor(Config) {
+ super(Config, Catagories.SENSOR);
+
+ this._service = new Service.LightSensor(Config.name, Config.name);
+
+ this._service.setCharacteristic(Characteristic.CurrentAmbientLightLevel, 25);
+ this._service.setCharacteristic(Characteristic.StatusActive, 1);
+ this._service.setCharacteristic(Characteristic.StatusFault, 0);
+ this._service.setCharacteristic(Characteristic.StatusTampered, 0);
+ this._Properties["CurrentAmbientLightLevel"] = 25;
+ this._Properties["StatusActive"] = 1;
+ this._Properties["StatusFault"] = 0;
+ this._Properties["StatusTampered"] = 0;
+
+ const EventStruct = {
+ "Get": ["CurrentAmbientLightLevel", "StatusActive", "StatusTampered", "StatusFault"],
+ "Set": []
+ }
+
+ this._wireUpEvents(this._service, EventStruct);
+ this._accessory.addService(this._service);
+
+ this._createBatteryService();
+ }
+}
+LightSensor.prototype.setCharacteristics = _setWithBattery;
+
+const AccessoryTypes = [{
+ Name: "TEMP",
+ Label: "Temperature Sensor",
+ Icon: "Ac_Temp.png",
+ SupportsRouting: false,
+ Class: Temperature,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Living Room Temp",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "SMOKE_SENSOR",
+ Label: "Smoke Alarm",
+ Icon: "Ac_Smoke.png",
+ SupportsRouting: false,
+ Class: Smoke,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Hallway Smoke Alarm",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "LIGHT_SENSOR",
+ Label: "Light Sensor",
+ Icon: "Ac_LightSensor.png",
+ SupportsRouting: false,
+ Class: LightSensor,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Porch Ambience Sensor",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "LEAK_SENSOR",
+ Label: "Leak Sensor",
+ Icon: "Ac_Leak.png",
+ SupportsRouting: false,
+ Class: Leak,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Kitchen Leak Sensor",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "FAN",
+ Label: "Smart Fan",
+ Icon: "Ac_FAN.png",
+ SupportsRouting: true,
+ Class: Fan,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Kitchen Extractor Fan",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "LIGHTBULB",
+ Label: "Smart Light Bulb",
+ Icon: "Ac_LIGHTBULB.png",
+ SupportsRouting: true,
+ Class: LightBulb,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Hallway Light",
+ Type: "text"
+ },
+ {
+ Name: "supportsBrightness",
+ Label: "Brightness Control",
+ Default: "true",
+ Type: "checkbox"
+ },
+ {
+ Name: "colorMode",
+ Label: "Color Mode",
+ Default: "hue",
+ Choices: ["hue", "temperature", "none"],
+ Type: "choice"
+ }
+ ]
+ },
+ {
+ Name: "THERMOSTAT",
+ Label: "Smart Thermostat",
+ Icon: "Ac_THERMOSTAT.png",
+ SupportsRouting: true,
+ Class: Thermostat,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Lounge Thermostat",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "GARAGE_DOOR",
+ Label: "Garage Door",
+ Icon: "Ac_GARAGE_DOOR.png",
+ SupportsRouting: true,
+ Class: GarageDoor,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Bugatti Veyron Garage",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "LOCK",
+ Label: "Smart Lock",
+ Icon: "Ac_LOCK.png",
+ SupportsRouting: true,
+ Class: Lock,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Comms Room Lock",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "MOTION_SENSOR",
+ Label: "Motion Sensor",
+ Icon: "Ac_MOTION_SENSOR.png",
+ SupportsRouting: false,
+ Class: Motion,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Entrance Hall Motion Sensor",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "CONTACT_SENSOR",
+ Label: "Contact Sensor",
+ Icon: "Ac_CONTACT_SENSOR.png",
+ SupportsRouting: false,
+ Class: Contact,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Loft Hatch",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "ALARM",
+ Label: "Security Alarm",
+ Icon: "Ac_ALARM.png",
+ SupportsRouting: true,
+ Class: Alarm,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Intruder Alarm",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "SWITCH",
+ Label: "On/Off Switch",
+ Icon: "Ac_SWITCH.png",
+ SupportsRouting: true,
+ Class: Switch,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Party Switch",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "OUTLET",
+ Label: "Electrical Outlet",
+ Icon: "Ac_OUTLET.png",
+ SupportsRouting: true,
+ Class: Outlet,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Hallway Socket",
+ Type: "text"
+ }]
+ },
+ {
+ Name: "TV",
+ Label: "Smart TV",
+ Icon: "Ac_TV.png",
+ SupportsRouting: true,
+ Class: TV,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Cinema Room Plasma",
+ Type: "text"
+ },
+ {
+ Name: "inputs",
+ Label: "Input Sources (1 per line)",
+ Default: ["HDMI 1", "HDMI 2", "HDMI 3"],
+ Type: "multi"
+ }
+ ]
+ },
+ {
+ Name: "CAMERA",
+ Label: "CCTV Camera",
+ Icon: "Ac_CAMERA.png",
+ SupportsRouting: false,
+ Class: Camera,
+ ConfigProperties: [{
+ Name: "name",
+ Label: "Accessory Name",
+ Default: "Garage Camera",
+ Type: "text"
+ },
+ {
+ Name: "enableMotionDetectionService",
+ Label: "Enable Motion Detection Feature",
+ Default: "false",
+ Type: "checkbox"
+ },
+ {
+ Name: "enableDoorbellService",
+ Label: "Enable Door Bell Feature",
+ Default: "false",
+ Type: "checkbox"
+ },
+ {
+ Name: "processor",
+ Label: "Video Processor",
+ Default: "ffmpeg",
+ Type: "text"
+ },
+ {
+ Name: "liveStreamSource",
+ Label: "Live Stream Input",
+ Default: "-rtsp_transport tcp -i rtsp://username:password@ip:port/StreamURI",
+ Type: "text"
+ },
+ {
+ Name: "stillImageSource",
+ Label: "Still Image Input",
+ Default: "http://username:password@ip:port/SnapshotURI",
+ Type: "text"
+ },
+ {
+ Name: "maxWidthHeight",
+ Label: "Max Width & Height (WxH)",
+ Default: "1280x720",
+ Type: "text"
+ },
+ {
+ Name: "maxFPS",
+ Label: "Max FPS",
+ Default: "10",
+ Type: "text"
+ },
+ {
+ Name: "maxStreams",
+ Label: "Max No Of Viewers",
+ Default: "2",
+ Type: "text"
+ },
+ {
+ Name: "encoder",
+ Label: "Video Encoder",
+ Default: "libx264",
+ Type: "text"
+ },
+ {
+ Name: "maxBitrate",
+ Label: "Max Bit Rate",
+ Default: "300",
+ Type: "text"
+ },
+ {
+ Name: "packetSize",
+ Label: "Max Packet Size",
+ Default: "1316",
+ Type: "text"
+ },
+ {
+ Name: "mapVideo",
+ Label: "Video Map",
+ Default: "0:0",
+ Type: "text"
+ },
+ {
+ Name: "additionalCommandline",
+ Label: "Additional Processor Args",
+ Default: "-tune zerolatency -preset ultrafast",
+ Type: "text"
+ },
+ {
+ Name: "adhereToRequestedSize",
+ Label: "Honor Requested Resolution",
+ Default: "true",
+ Type: "checkbox"
+ },
+ {
+ Name: "enableAudio",
+ Label: "Enable Audio Streaming",
+ Default: "false",
+ Type: "checkbox"
+ },
+ {
+ Name: "encoder_audio",
+ Label: "Audio Encoder",
+ Default: "libfdk_aac",
+ Type: "text"
+ },
+ {
+ Name: "mapAudio",
+ Label: "Audio Map",
+ Default: "0:1",
+ Type: "text"
+ }
+ ]
+ }
+]
+
+module.exports = {
+
+ Types: AccessoryTypes,
+ Bridge: Bridge
+
+}
\ No newline at end of file
diff --git a/core/cameraSource.js b/core/cameraSource.js
new file mode 100644
index 0000000..94672bb
--- /dev/null
+++ b/core/cameraSource.js
@@ -0,0 +1,324 @@
+'use strict'
+const HapNodeJS = require('hap-nodejs')
+const uuid = HapNodeJS.uuid
+const ip = require('ip')
+const CameraController = HapNodeJS.CameraController;
+const spawn = require('child_process').spawn
+
+const Camera = function(Config) {
+ this.config = Config;
+ this.controller = null;
+ this.pendingSessions = {}
+ this.ongoingSessions = {}
+ this.maxFPS = Config.maxFPS > 30 ? 30 : Config.maxFPS
+ this.maxWidth = Config.maxWidthHeight.split("x")[0];
+ this.maxHeight = Config.maxWidthHeight.split("x")[1];
+ this.maxBitrate = Config.maxBitrate;
+ this.maxPacketSize = Config.packetSize;
+ this.lastSnapshotTime;
+ this.imageCache;
+
+}
+
+Camera.prototype.attachController = function(Controller) {
+ this.controller = Controller;
+}
+
+Camera.prototype.handleSnapshotRequest = function(request, callback) {
+
+ if (this.lastSnapshotTime != null) {
+ const Now = new Date().getTime();
+ const Diff = (Now - this.lastSnapshotTime) / 1000;
+
+ // 1 Minute snapshot cache
+ if (parseInt(Diff) < 60) {
+ callback(null, this.imageCache)
+ return;
+ }
+
+ }
+
+ let imageBuffer = Buffer.alloc(0);
+
+ const CMD = [];
+ CMD.push('-analyzeduration 1')
+ CMD.push(this.config.stillImageSource)
+ CMD.push('-s ' + request.width + 'x' + request.height)
+ CMD.push('-vframes 1')
+ CMD.push('-f image2')
+ CMD.push('-')
+
+ const ffmpeg = spawn(this.config.processor, CMD.join(' ').split(' '), {
+ env: process.env
+ })
+
+ ffmpeg.stdout.on('data', function(data) {
+ imageBuffer = Buffer.concat([imageBuffer, data])
+ })
+
+ // We need to get access to context here, hence the more recent arrow function approach
+ ffmpeg.on('close', (c) => {
+ this.lastSnapshotTime = new Date().getTime();
+ this.imageCache = null;
+ this.imageCache = imageBuffer
+
+ callback(null, this.imageCache)
+
+ })
+
+}
+
+Camera.prototype.prepareStream = function(request, callback) {
+ const sessionInfo = {}
+
+ const sessionID = request['sessionID']
+ sessionInfo['address'] = request['targetAddress']
+
+ const response = {}
+
+ const videoInfo = request['video']
+ if (videoInfo) {
+ const targetPort = videoInfo['port']
+ const srtp_key = videoInfo['srtp_key']
+ const srtp_salt = videoInfo['srtp_salt']
+ const ssrc = CameraController.generateSynchronisationSource();
+ response['video'] = {
+ port: targetPort,
+ ssrc: ssrc,
+ srtp_key: srtp_key,
+ srtp_salt: srtp_salt
+ }
+ sessionInfo['video_port'] = targetPort
+ sessionInfo['video_srtp'] = Buffer.concat([srtp_key, srtp_salt])
+ sessionInfo['video_ssrc'] = ssrc
+ }
+
+ const audioInfo = request['audio']
+ if (audioInfo) {
+ const targetPort = audioInfo['port']
+ const srtp_key = audioInfo['srtp_key']
+ const srtp_salt = audioInfo['srtp_salt']
+ const ssrc = CameraController.generateSynchronisationSource();
+ response['audio'] = {
+ port: targetPort,
+ ssrc: ssrc,
+ srtp_key: srtp_key,
+ srtp_salt: srtp_salt
+ }
+ sessionInfo['audio_port'] = targetPort
+ sessionInfo['audio_srtp'] = Buffer.concat([srtp_key, srtp_salt])
+ sessionInfo['audio_ssrc'] = ssrc
+ }
+
+ const currentAddress = ip.address()
+
+ const addressResp = {
+
+ address: currentAddress,
+ }
+
+ if (ip.isV4Format(currentAddress)) {
+ addressResp['type'] = 'v4'
+ } else {
+ addressResp['type'] = 'v6'
+ }
+
+ response['address'] = addressResp
+
+ this.pendingSessions[uuid.unparse(sessionID)] = sessionInfo
+ callback(null, response);
+}
+
+Camera.prototype.handleStreamRequest = function(request, callback) {
+ const sessionID = request['sessionID']
+ const requestType = request['type']
+
+ if (sessionID) {
+ const sessionIdentifier = uuid.unparse(sessionID)
+ switch (requestType) {
+ case "reconfigure":
+ // to do
+ callback(null);
+ break;
+
+ case "stop":
+ const ffmpegProcess = this.ongoingSessions[sessionIdentifier]
+ if (ffmpegProcess) {
+ ffmpegProcess.kill('SIGKILL')
+ }
+ delete this.ongoingSessions[sessionIdentifier]
+ callback(null);
+ break;
+
+ case "start":
+ const sessionInfo = this.pendingSessions[sessionIdentifier]
+
+ if (sessionInfo) {
+ let width = this.maxWidth;
+ let height = this.maxHeight;
+ let FPS = this.maxFPS;
+ let bitRate = this.maxBitrate
+ let aBitRate = 24
+ let aSampleRate = HapNodeJS.AudioStreamingSamplerate.KHZ_16
+ let VPT = 0;
+ let APT = 0;
+ let MaxPaketSize = this.maxPacketSize
+
+ const videoInfo = request['video']
+ if (videoInfo) {
+ width = videoInfo['width']
+ height = videoInfo['height']
+ VPT = videoInfo["pt"];
+
+ const expectedFPS = videoInfo['fps']
+ if (expectedFPS < FPS) {
+ FPS = expectedFPS
+ }
+
+ if (videoInfo['max_bit_rate'] < bitRate) {
+ bitRate = videoInfo['max_bit_rate']
+ }
+
+ if (videoInfo["mtu"] < MaxPaketSize) {
+ MaxPaketSize = videoInfo["mtu"];
+ }
+ }
+
+ const audioInfo = request['audio']
+ if (audioInfo) {
+ if (audioInfo['max_bit_rate'] < aBitRate) {
+ aBitRate = audioInfo['max_bit_rate']
+ }
+
+ if (audioInfo['sample_rate'] < aSampleRate) {
+ aSampleRate = audioInfo['sample_rate']
+ }
+
+ APT = audioInfo["pt"];
+ }
+
+ const targetAddress = sessionInfo['address']
+ const targetVideoPort = sessionInfo['video_port']
+ const videoKey = sessionInfo['video_srtp']
+ const videoSsrc = sessionInfo['video_ssrc']
+ const targetAudioPort = sessionInfo['audio_port']
+ const audioKey = sessionInfo['audio_srtp']
+ const audioSsrc = sessionInfo['audio_ssrc']
+
+ const CMD = [];
+
+ // Input
+ CMD.push(this.config.liveStreamSource)
+ CMD.push('-map ' + this.config.mapVideo)
+ CMD.push('-vcodec ' + this.config.encoder)
+ CMD.push('-pix_fmt yuv420p')
+ CMD.push('-r ' + FPS)
+ CMD.push('-f rawvideo')
+
+ if (this.config.additionalCommandline.length > 0) {
+ CMD.push(this.config.additionalCommandline);
+ }
+
+ if (this.config.adhereToRequestedSize == 'true' && this.config.encoder != 'copy') {
+ CMD.push('-vf scale=' + width + ':' + height)
+ }
+
+ CMD.push('-b:v ' + bitRate + 'k')
+ CMD.push('-bufsize ' + bitRate + 'k')
+ CMD.push('-maxrate ' + bitRate + 'k')
+ CMD.push('-payload_type ' + VPT)
+
+ // Output
+ CMD.push('-ssrc ' + videoSsrc)
+ CMD.push('-f rtp')
+ CMD.push('-srtp_out_suite AES_CM_128_HMAC_SHA1_80')
+ CMD.push('-srtp_out_params ' + videoKey.toString('base64'))
+ CMD.push('srtp://' + targetAddress + ':' + targetVideoPort + '?rtcpport=' + targetVideoPort + '&pkt_size=' + MaxPaketSize)
+
+ // Audio ?
+ if (this.config.enableAudio == 'true') {
+ // Input
+ CMD.push('-map ' + this.config.mapAudio)
+ CMD.push('-acodec ' + this.config.encoder_audio)
+ CMD.push('-profile:a aac_eld')
+ CMD.push('-flags +global_header')
+ CMD.push('-f null');
+ CMD.push('-ar ' + aSampleRate + 'k')
+ CMD.push('-b:a ' + aBitRate + 'k')
+ CMD.push('-bufsize ' + aBitRate + 'k')
+ CMD.push('-ac 1')
+ CMD.push('-payload_type ' + APT)
+
+ // Output
+ CMD.push('-ssrc ' + audioSsrc)
+ CMD.push('-f rtp')
+ CMD.push('-srtp_out_suite AES_CM_128_HMAC_SHA1_80')
+ CMD.push('-srtp_out_params ' + audioKey.toString('base64'))
+ CMD.push('srtp://' + targetAddress + ':' + targetAudioPort + '?rtcpport=' + targetAudioPort + '&pkt_size=188')
+ }
+
+ const ffmpeg = spawn(this.config.processor, CMD.join(' ').split(' '), {
+ env: process.env,
+ stdout: 'ignore'
+ })
+
+ let live = false;
+ let CBCalled = false;
+
+ ffmpeg.stderr.on('data', data => {
+ if (!live) {
+ if (data.toString().includes('frame=')) {
+ live = true;
+ CBCalled = true;
+ callback(null);
+
+ this.ongoingSessions[sessionIdentifier] = ffmpeg
+ delete this.pendingSessions[sessionIdentifier]
+ }
+ }
+ });
+
+ ffmpeg.on('error', error => {
+ if (!live) {
+ if (!CBCalled) {
+ callback(new Error('FFMPEG Error : ' + error.message));
+ CBCalled = true;
+ }
+
+ delete this.pendingSessions[sessionIdentifier]
+ } else {
+ this.controller.forceStopStreamingSession(sessionID);
+ if (this.ongoingSessions.hasOwnProperty(sessionIdentifier)) {
+ delete this.ongoingSessions[sessionIdentifier]
+ }
+ }
+
+ });
+
+ ffmpeg.on('exit', (c, s) => {
+ if (c != null && c != 255) {
+ if (!live) {
+ if (!CBCalled) {
+ callback(new Error('FFMPEG Exit : Code - ' + c + ', Signal - ' + s));
+ CBCalled = true;
+ }
+
+ delete this.pendingSessions[sessionIdentifier]
+ } else {
+ this.controller.forceStopStreamingSession(sessionID);
+ if (this.ongoingSessions.hasOwnProperty(sessionIdentifier)) {
+ delete this.ongoingSessions[sessionIdentifier]
+ }
+ }
+ }
+ });
+
+ }
+ break;
+ }
+ }
+}
+
+module.exports = {
+ Camera: Camera,
+}
\ No newline at end of file
diff --git a/core/mqtt.js b/core/mqtt.js
new file mode 100644
index 0000000..9cc1dea
--- /dev/null
+++ b/core/mqtt.js
@@ -0,0 +1,79 @@
+'use strict'
+const mqtt = require('mqtt')
+const util = require('./util');
+const config = require(util.ConfigPath);
+
+var MQTTC;
+var _Accessories;
+var CallBack;
+
+const MQTTError = function(Error) {
+ console.log(" Could not connect to MQTT Broker : " + Error);
+ process.exit(0)
+}
+
+const MQTTConnected = function(Client) {
+ MQTTC = Client;
+ MQTTC.subscribe(config.MQTTTopic, MQTTSubscribeDone)
+}
+
+const MQTTSubscribeDone = function(error) {
+ if (!error) {
+ MQTTC.on('message', MQTTMessageReceved)
+ CallBack();
+ } else {
+ console.log(" Could not subscribe to Topic : " + err);
+ process.exit(0)
+ }
+}
+
+const MQTTMessageReceved = function(topic, message) {
+ try {
+ const sPL = message.toString();
+ const PL = JSON.parse(sPL);
+ const TargetAccessory = topic.split('/').pop()
+
+ const Ac = _Accessories[TargetAccessory]
+
+ if (Ac != null) {
+ Ac.setCharacteristics(PL)
+ }
+ } catch (e) {
+ console.log(" MQTT input could not be actioned -> MSG: " + sPL + ", Accessory ID: " + TargetAccessory + "");
+ }
+
+}
+
+const MQTT = function(Accesories, CB) {
+ _Accessories = Accesories;
+ CallBack = CB;
+
+ if (config.hasOwnProperty("enableIncomingMQTT") && config.enableIncomingMQTT == 'true') {
+ if (!config.hasOwnProperty("MQTTOptions")) {
+ config.MQTTOptions = {};
+ } else if (config.MQTTOptions.username.length < 1) {
+ delete config.MQTTOptions["username"]
+ delete config.MQTTOptions["password"]
+ }
+
+ console.log(" Starting MQTT Client")
+
+ try {
+ const _MQTTC = mqtt.connect(config.MQTTBroker, config.MQTTOptions)
+ _MQTTC.on('error', MQTTError);
+ _MQTTC.on('connect', () => MQTTConnected(_MQTTC))
+
+ } catch (err) {
+ console.log(" Could not connect to MQTT Broker : " + err);
+ process.exit(0);
+ }
+
+ } else {
+ CallBack();
+ }
+
+}
+
+module.exports = {
+ MQTT: MQTT
+}
\ No newline at end of file
diff --git a/core/routes.js b/core/routes.js
new file mode 100644
index 0000000..8f6e2be
--- /dev/null
+++ b/core/routes.js
@@ -0,0 +1,167 @@
+'use strict'
+const fs = require('fs');
+const dgram = require("dgram");
+const mqtt = require('mqtt')
+const axios = require('axios')
+const util = require('./util')
+const Websocket = require('ws')
+const Path = require('path');
+
+var UDPServer;
+const MQTTCs = {};
+const Websockets = {}
+
+/* Clean Payload */
+const CleanPayload = function(Payload, Type) {
+ const Copy = JSON.parse(JSON.stringify(Payload));
+
+ Copy["route_type"] = Type;
+ Copy["route_name"] = Payload.accessory.route
+
+ delete Copy.accessory.pincode;
+ delete Copy.accessory.username;
+ delete Copy.accessory.setupID;
+ delete Copy.accessory.route;
+ delete Copy.accessory.description;
+ delete Copy.accessory.serialNumber;
+
+ return Copy;
+
+}
+
+/* WS */
+const WEBSOCKET = function(route, payload) {
+ payload = CleanPayload(payload, "WEBSOCKET")
+
+ if (Websockets.hasOwnProperty(route.uri)) {
+ Websockets[route.uri].send(JSON.stringify(payload));
+ } else {
+ const WS = new Websocket(route.uri);
+ WS.on('open', () => HandleWSOpen(route, WS, payload));
+ WS.on('error', (e) => WSError(e))
+ }
+
+}
+
+const WSError = function(err) {
+ console.log(" Could not connect to Websocket : " + err);
+}
+
+const HandleWSOpen = function(route, WS, Payload) {
+
+ Websockets[route.uri] = WS;
+ Websockets[route.uri].send(JSON.stringify(Payload));
+}
+
+/* HTTP */
+const HTTP = function(route, payload) {
+ payload = CleanPayload(payload, "HTTP")
+
+ const CFG = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Homekit Device Stack'
+ },
+ url: route.destinationURI.replace('{{accessoryID}}', payload.accessory.accessoryID),
+ method: 'post',
+ data: payload
+ }
+
+ axios.request(CFG)
+ .then(function(res) {})
+ .catch(function(err) {
+ console.log(" Could not send HTTP request : " + err);
+ })
+}
+
+/* UDP */
+const UDP = function(route, payload) {
+ payload = CleanPayload(payload, "UDP");
+ let JSONs = JSON.stringify(payload);
+
+ if (UDPServer == null) {
+ UDPServer = dgram.createSocket("udp4");
+ UDPServer.bind(() => UDPConnected(JSONs, route))
+ } else {
+ UDPServer.send(JSONs, 0, JSONs.length, route.port, route.address, UDPDone)
+ }
+}
+
+const UDPConnected = function(JSONString, Route) {
+ UDPServer.setBroadcast(true);
+ UDPServer.send(JSONString, 0, JSONString.length, Route.port, Route.address, UDPDone);
+}
+
+const UDPDone = function(e, n) {
+ if (e) {
+ console.log(" Could not broadcast UDP: " + e);
+ }
+}
+
+/* MQTT */
+const MQTT = function(route, payload) {
+ payload = CleanPayload(payload, "MQTT");
+ let JSONs = JSON.stringify(payload);
+
+ if (MQTTCs.hasOwnProperty(route.broker)) {
+ let Topic = route.topic.replace('{{accessoryID}}', payload.accessory.accessoryID);
+ MQTTCs[route.broker].publish(Topic, JSONs, null, MQTTDone);
+ } else {
+ if (!route.hasOwnProperty("MQTTOptions")) {
+ route.MQTTOptions = {};
+ } else if (route.MQTTOptions.hasOwnProperty("username") && route.MQTTOptions.username.length < 1) {
+ delete route.MQTTOptions["username"]
+ delete route.MQTTOptions["password"]
+ }
+ const MQTTC = mqtt.connect(route.broker, route.MQTTOptions)
+ let Topic = route.topic.replace('{{accessoryID}}', payload.accessory.accessoryID);
+ MQTTC.on('error', MQTTError);
+ MQTTC.on('connect', () => MQTTConnected(JSONs, route, Topic, MQTTC));
+ }
+}
+
+const MQTTError = function(err) {
+ console.log(" Could not connect to MQTT Broker : " + err);
+}
+
+const MQTTConnected = function(JSONString, Route, Topic, Client) {
+ MQTTCs[Route.broker] = Client;
+ MQTTCs[Route.broker].publish(Topic, JSONString, null, MQTTDone);
+}
+
+const MQTTDone = function() {}
+
+/* FILE */
+const FILE = function(route, payload) {
+ payload = CleanPayload(payload, "FILE");
+
+ let DirPath = Path.join(util.RootPath, route.directory.replace('{{accessoryID}}', payload.accessory.accessoryID))
+
+ fs.mkdirSync(DirPath, {
+ recursive: true
+ }, function(err) {
+ if (err) {
+ console.log(" Could not write output to file.");
+ }
+ });
+
+ const DT = new Date().getTime();
+ const FileName = DT + '_' + payload.accessory.accessoryID + ".json"
+ let _Path = Path.join(DirPath, FileName);
+
+ fs.writeFile(_Path, JSON.stringify(payload), 'utf8', FileDone);
+}
+
+const FileDone = function(err) {
+ if (err) {
+ console.log(" Could not write output to file.");
+ }
+}
+
+module.exports = {
+ "HTTP": HTTP,
+ "UDP": UDP,
+ "FILE": FILE,
+ "MQTT": MQTT,
+ "WEBSOCKET": WEBSOCKET
+}
\ No newline at end of file
diff --git a/core/routing.js b/core/routing.js
new file mode 100644
index 0000000..92c193f
--- /dev/null
+++ b/core/routing.js
@@ -0,0 +1,82 @@
+'use strict'
+const PATH = require('path');
+const FS = require('fs');
+const { spawnSync } = require('child_process');
+
+const StockRoutes = ["hkds-route-console", "hkds-route-file", "hkds-route-http", "hkds-route-mqtt", "hkds-route-udp", "hkds-route-websocket"]
+var RootPath;
+
+const Routes = {
+
+}
+
+const setPath = function(Path){
+ RootPath = Path;
+}
+
+const installStockModules = function () {
+
+ module.paths.push(PATH.join(RootPath, "node_modules"))
+
+ let FoundModules = []
+
+ if (FS.existsSync(PATH.join(RootPath, "node_modules"))) {
+
+ let Files = FS.readdirSync(PATH.join(RootPath, "node_modules"));
+
+ Files.forEach((D) => {
+ let FI = FS.lstatSync(PATH.join(RootPath, "node_modules", D))
+ if (FI.isDirectory()) {
+ FoundModules.push(D)
+ }
+ })
+ }
+
+ StockRoutes.forEach((SR) => {
+ if (FoundModules.indexOf(SR) < 0) {
+ install(SR)
+ }
+ })
+}
+
+const loadModules = function (){
+
+
+ let Files = FS.readdirSync(PATH.join(RootPath, "node_modules"));
+
+ Files.forEach((D) => {
+
+ if(!D.startsWith("hkds-route-")){
+ return;
+ }
+
+ let FI = FS.lstatSync(PATH.join(RootPath, "node_modules", D))
+ if (FI.isDirectory()) {
+
+ let Mod = require(D);
+ let RouteOBJ = {}
+
+ RouteOBJ.Type = D
+ RouteOBJ.Icon = PATH.join(RootPath,"node_modules",D,Mod.Icon);
+ RouteOBJ.Name = Mod.Name;
+ RouteOBJ.Class = Mod.Route;
+ RouteOBJ.Inputs = Mod.Inputs;
+
+ Routes[D] = RouteOBJ;
+ }
+ })
+
+}
+
+const install = function(Module) {
+ console.log(" Installing route module: " + Module);
+ spawnSync("npm", ["install", "" + Module + "", "--prefix", '"' + RootPath + '"'], { shell: true });
+}
+
+module.exports = {
+ "installStockModules": installStockModules,
+ "Routes": Routes,
+ "loadModules":loadModules,
+ "install": install,
+ "setPath":setPath
+}
diff --git a/core/server.js b/core/server.js
new file mode 100755
index 0000000..e29543e
--- /dev/null
+++ b/core/server.js
@@ -0,0 +1,652 @@
+'use strict'
+const EXPRESS = require('express')
+const CRYPTO = require('crypto')
+const HANDLEBARS = require('handlebars')
+const FS = require('fs');
+const BODYPARSER = require('body-parser')
+const ACCESSORY = require('./accessory');
+const UTIL = require('./util');
+const CONFIG = require(UTIL.ConfigPath);
+const COOKIEPARSER = require('cookie-parser')
+const PATH = require('path');
+const OS = require("os");
+const ROUTING = require('./routing');
+
+const Server = function(Accesories, ChangeEvent, IdentifyEvent, Bridge, RouteSetup, PairEvent) {
+
+ // Vars
+ let _Paired = false;
+ const _Accessories = Accesories
+ const _ChangeEvent = ChangeEvent;
+ const _IdentifyEvent = IdentifyEvent
+ const _Bridge = Bridge;
+ const _RouteSetup = RouteSetup
+ const _PairEvent = PairEvent;
+
+ // Template Files
+ const Templates = {
+ "Login": process.cwd() + "/ui/login.tpl",
+ "Setup": process.cwd() + "/ui/setup.tpl",
+ "Main": process.cwd() + "/ui/main.tpl",
+ "Create": process.cwd() + "/ui/create.tpl",
+ "Edit": process.cwd() + "/ui/edit.tpl",
+ }
+
+ HANDLEBARS.registerHelper('ifvalue', function(conditional, options) {
+ if (options.hash.equals === conditional) {
+ return options.fn(this)
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ const CompiledTemplates = {}
+
+ // Start Server
+ this.Start = function(CB) {
+
+ console.log(" Starting Web Server")
+ console.log(" ")
+
+ let TemplateKeys = Object.keys(Templates)
+
+ // Compile TPLs
+ for (let i = 0; i < TemplateKeys.length; i++) {
+ CompiledTemplates[TemplateKeys[i]] = HANDLEBARS.compile(FS.readFileSync(Templates[TemplateKeys[i]], 'utf8'));
+ }
+
+ // Express
+ const app = EXPRESS()
+
+ // Middleware
+ app.use(BODYPARSER.json())
+ app.use(COOKIEPARSER("2jS4khgKVTMaVhwVxYPx8Kjnwwpfyvxa"))
+
+ // UI
+ app.use('/ui/static', EXPRESS.static(process.cwd() + '/ui/static'))
+ app.get('/', _Redirect);
+ app.get('/ui/main', _Main);
+ app.get('/ui/setup', _Setup);
+ app.get('/ui/getroutemeta/:module_name', _GetRouteMeta);
+ app.get('/ui/login', _Login);
+ app.get('/ui/createaccessory/:type', _CreateAccessory);
+ app.get('/ui/editaccessory/:type/:id', _EditAccessory);
+ app.get('/ui/pairstatus', _PairStatus);
+ app.get('/ui/pairstatus/:id', _PairStatus);
+ app.get('/ui/backup', _Backup);
+ app.post('/ui/login', _DoLogin);
+ app.post('/ui/deleteroute', _DoDeleteRoute);
+ app.post('/ui/setconfig', _DoSaveConfig);
+ app.post('/ui/deleteaccessory', _DoDeleteAccessory);
+ app.post('/ui/restore', _DoRestore);
+ app.post('/ui/createroute', _DoCreateRoute);
+ app.post('/ui/connect', _DoConnect);
+ app.post('/ui/disconnect', _DoDisconnect);
+ app.post('/ui/createaccessory', _DoCreateAccessory);
+ app.post('/ui/editaccessory', _DoEditAccessory);
+
+ // API
+ app.get('/:pwd/accessories/', _processAccessoriesGet);
+ app.get('/:pwd/accessories/:id', _processAccessoryGet);
+ app.put('/:pwd/accessories/:id', _processAccessorySet);
+
+ try {
+ if (CONFIG.webInterfaceAddress == 'ALL') {
+ app.listen(CONFIG.webInterfacePort)
+ } else {
+ app.listen(CONFIG.webInterfacePort, CONFIG.webInterfaceAddress)
+ }
+
+ } catch (err) {
+ console.log(" Could not start Web Server : " + err);
+ process.exit(0);
+ }
+
+ CB();
+ }
+
+ function _GetRouteMeta(req,res){
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let ModuleName = req.params.module_name;
+ let RouteType = ROUTING.Routes[ModuleName];
+
+ let _Buffer = FS.readFileSync(RouteType.Icon)
+
+ let MD = {
+ Inputs:RouteType.Inputs,
+ Icon:_Buffer.toString("base64"),
+ Name:RouteType.Name
+ }
+
+ res.contentType('application/json');
+ res.send(MD);
+
+ }
+
+ function _Redirect(req,res){
+ res.redirect('./ui/main')
+ }
+
+ function _DoEditAccessory(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Data = req.body
+
+ const Acs = _Accessories[Data.username.replace(/:/g, "")];
+ const CurrentProps = Acs.getProperties();
+
+ _DeleteAccessory(Data.username.replace(/:/g, ""), Data.username, Data.bridged, false);
+
+ let URI = _AddAccessory(Data, CurrentProps);
+ res.contentType('application/json');
+ if (!Data.bridged) {
+ res.send('{"OK":true,"URI":"' + URI + '","Pin":"' + Data.pincode + '","ID":"' + Data.username.replace(/:/g, "") + '"}');
+ } else {
+ res.send('{"OK":true}');
+ }
+
+ }
+
+ function _DoCreateAccessory(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Data = req.body
+
+ Data["pincode"] = UTIL.getRndInteger(100, 999) + "-" + UTIL.getRndInteger(10, 99) + "-" + UTIL.getRndInteger(100, 999);
+ Data["username"] = UTIL.genMAC();
+ Data["setupID"] = UTIL.makeID(4);
+ Data["serialNumber"] = UTIL.makeID(12);
+
+ let URI = _AddAccessory(Data);
+ res.contentType('application/json');
+ if (!Data.bridged) {
+ res.send('{"OK":true,"URI":"' + URI + '","Pin":"' + Data.pincode + '","ID":"' + Data.username.replace(/:/g, "") + '"}');
+ } else {
+ res.send('{"OK":true}');
+ }
+ }
+
+ function _DoConnect(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Data = req.body
+
+ _ReRoute(Data.SID, Data.TID);
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+
+ }
+
+ function _DoDisconnect(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Data = req.body
+
+ _Unroute(Data.SID);
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+
+ }
+
+ function _DoCreateRoute(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Data = req.body
+
+ _AddRoute(Data);
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+
+ }
+
+ function _DoRestore(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Result = UTIL.restore(req.body);
+
+ if (Result == "Version") {
+ res.contentType('application/json');
+ res.send('{"OK":false,"Reason":"VersionMismatch"}');
+ return;
+ }
+ if (Result == "Invalid") {
+ res.contentType('application/json');
+ res.send('{"OK":false,"Reason":"InvalidFile"}');
+ return;
+ }
+
+ if (Result == true) {
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+ process.exit(0);
+ }
+
+ }
+
+ function _DoDeleteAccessory(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ const Data = req.body;
+
+ _DeleteAccessory(Data.username.replace(/:/g, ""), Data.username, Data.bridged, true);
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+
+ }
+
+ function _DoSaveConfig(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ const Data = req.body;
+
+ UTIL.updateOptions(Data);
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+
+ }
+
+ function _DoLogin(req, res) {
+
+ const Data = req.body;
+
+ const Username = Data.username;
+ const Password = CRYPTO.createHash('md5').update(Data.password).digest("hex");
+ if (Username == CONFIG.loginUsername && Password == CONFIG.loginPassword) {
+ res.cookie('Authentication', 'Success', {
+ 'signed': true
+ })
+ res.contentType('application/json');
+ let Response = {
+ success: true,
+ destination: '../../../ui/main'
+ }
+ res.send(JSON.stringify(Response))
+ } else {
+ res.contentType('application/json');
+ let Response = {
+ success: false
+ }
+ res.send(JSON.stringify(Response))
+ }
+
+ }
+
+ function _DoDeleteRoute(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ const Data = req.body;
+ _DeleteRoute(Data.name)
+ res.contentType('application/json');
+ res.send('{"OK":true}');
+
+ }
+
+ /* Main Page */
+ function _Main(req, res) {
+
+ // Auth, Setup Check
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ // Availbale Route Types
+ let RouteModules = [];
+ let RouteTypes = Object.keys(ROUTING.Routes);
+ RouteTypes.forEach((RT) =>{
+
+ let RouteType = ROUTING.Routes[RT];
+ RouteModules.push({Type:RouteType.Type,Name:RouteType.Name})
+
+ });
+
+ // Configured Routes
+ let ConfiguredRoutesArray = [];
+ let ConfiguredRoutesKeys = Object.keys(CONFIG.routes);
+ ConfiguredRoutesKeys.forEach((CR) =>{
+
+ let ConfiguredRoute = CONFIG.routes[CR];
+
+
+ })
+
+
+
+
+
+ let Interfaces = OS.networkInterfaces();
+ let Keys = Object.keys(Interfaces);
+ let IPs = [];
+
+ for (let i = 0; i < Keys.length; i++) {
+
+ let Net = Interfaces[Keys[i]];
+
+ Net.forEach((AI) => {
+ if (AI.family == 'IPv4' && !AI.internal) {
+ IPs.push(AI.address)
+ }
+ })
+
+ }
+
+ let HTML = CompiledTemplates['Main']({
+ "RootPath": UTIL.RootPath,
+ "Config": CONFIG,
+ "RouteModules": RouteModules,
+ "AccessoryTypes": ACCESSORY.Types,
+ "AccessoryTypesJSON": JSON.stringify(ACCESSORY.Types, null, 2),
+ "interfaces": IPs
+ });
+ res.contentType('text/html')
+ res.send(HTML)
+
+ }
+
+ /* Setup Page */
+ function _Setup(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+ if (_Paired) {
+ res.redirect("../../../ui/main");
+ return;
+ }
+
+ let HTML = CompiledTemplates['Setup']({
+ "Config": CONFIG
+ });
+ res.contentType('text/html')
+ res.send(HTML)
+
+ }
+
+ /* Login Page */
+ function _Login(req, res) {
+
+ let HTML = CompiledTemplates['Login']({});
+
+ res.contentType('text/html')
+ res.send(HTML)
+
+ }
+
+ /* Create Page */
+ function _CreateAccessory(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Type = req.params.type;
+
+ let HTML = CompiledTemplates['Create']({
+ "Config": CONFIG,
+ "Type": JSON.stringify(ACCESSORY.Types.filter(C => C.Name == Type)[0], null, 2)
+ });
+
+ res.contentType('text/html')
+ res.send(HTML)
+
+ }
+
+ // Edit Pgae */
+ function _EditAccessory(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let Type = req.params.type;
+ let ID = req.params.id;
+
+ const TargetAc = CONFIG.accessories.filter(a => a.accessoryID == ID)[0]
+
+ let HTML = CompiledTemplates['Edit']({
+ "Config": CONFIG,
+ "Object": JSON.stringify(TargetAc, null, 2),
+ "Type": JSON.stringify(ACCESSORY.Types.filter(C => C.Name == Type)[0], null, 2)
+ })
+
+ res.contentType('text/html')
+ res.send(HTML)
+ }
+
+ // Pair Status */
+ function _PairStatus(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+
+ let ID = req.params.id;
+
+ let Response = {}
+
+ if (ID != null) {
+ const AccessoryFileName = PATH.join(UTIL.HomeKitPath, "AccessoryInfo." + ID + ".json");
+ if (FS.existsSync(AccessoryFileName)) {
+ delete require.cache[require.resolve(AccessoryFileName)];
+ const IsPaired = Object.keys(require(AccessoryFileName).pairedClients)
+ Response.paired = IsPaired.length > 0;
+ } else {
+ Response.paired = false;
+ }
+ } else {
+ Response.paired = _Paired
+ }
+ res.contentType('application/json');
+ res.send(JSON.stringify(Response))
+ }
+
+ function _Backup(req, res) {
+
+ if (!_CheckAuth(req, res)) {
+ return;
+ }
+ res.contentType('application/octet-stream')
+ res.header("Content-Disposition", "attachment; filename=\"HKDS-Backup.dat\"");
+ res.send(UTIL.generateBackup())
+ }
+
+ // Add new accessory
+ function _AddAccessory(Data, Props) {
+
+ UTIL.appendAccessoryToConfig(Data)
+ Data.accessoryID = Data.username.replace(/:/g, "");
+ CONFIG.accessories.push(Data)
+
+ let Type = ACCESSORY.Types.filter(C => C.Name == Data.type)[0]
+ let Acc = new Type.Class(Data);
+
+ if (Props != null) {
+ Acc.setCharacteristics(Props)
+ }
+
+ Acc.on('STATE_CHANGE', (PL, O) => _ChangeEvent(PL, Data, O))
+ Acc.on('IDENTIFY', (P) => _IdentifyEvent(P, Data))
+ _Accessories[Data.accessoryID] = Acc;
+ if (Data.bridged) {
+ _Bridge.addAccessory(Acc.getAccessory())
+ } else {
+ Acc.on('PAIR_CHANGE', (P) => _PairEvent(P, Data))
+ Acc.publish();
+ }
+ return Acc.getAccessory().setupURI();
+ }
+
+ // Delete Route
+ function _DeleteRoute(Name) {
+ let Routes = CONFIG.routes;
+ delete Routes[Name]
+ _RouteSetup();
+ UTIL.updateRouteConfig(Routes);
+ }
+
+ // Add Route
+ function _AddRoute(Route) {
+ let Routes = CONFIG.routes;
+ let Name = Route.name;
+ delete Route.name;
+ Routes[Name] = Route;
+ _RouteSetup();
+ UTIL.updateRouteConfig(Routes);
+ }
+
+ // Update Device Route
+ function _ReRoute(AccessoryID, Route) {
+ CONFIG.accessories.filter((A) => A.accessoryID == AccessoryID)[0].route = Route.split('_')[1];
+ UTIL.routeAccessory(AccessoryID, Route.split('_')[1]);
+ }
+
+ // Clear Device Route
+ function _Unroute(AccessoryID) {
+ CONFIG.accessories.filter((A) => A.accessoryID == AccessoryID)[0].route = "";
+ UTIL.routeAccessory(AccessoryID, "");
+ }
+
+ // Delete Accessory
+ function _DeleteAccessory(AccessoryID, Username, Bridged, Permanent) {
+
+ if (Bridged) {
+ const Acs = _Bridge.getAccessories();
+ const TargetAcc = Acs.filter(a => a.username == Username)[0];
+ _Bridge.removeAccessory(TargetAcc);
+ delete _Accessories[AccessoryID]
+ UTIL.deleteAccessory(AccessoryID)
+ const NA = CONFIG.accessories.filter(a => a.accessoryID != AccessoryID)
+ CONFIG.accessories = NA;
+ } else {
+ _Accessories[AccessoryID].unpublish(Permanent)
+ delete _Accessories[AccessoryID]
+ UTIL.deleteAccessory(AccessoryID)
+ const NA = CONFIG.accessories.filter(a => a.accessoryID != AccessoryID)
+ CONFIG.accessories = NA;
+ }
+ }
+
+ // Check Auth
+ function _CheckAuth(req, res) {
+ if (req.signedCookies.Authentication == null || req.signedCookies.Authentication != 'Success') {
+ res.redirect("../../../ui/login");
+ return false;
+ }
+ return true;
+ }
+
+ // get Accessories
+ function _processAccessoriesGet(req, res) {
+ const PW = CRYPTO.createHash('md5').update(req.params.pwd).digest("hex");
+ if (PW != CONFIG.loginPassword) {
+ res.sendStatus(401);
+ return;
+ }
+ const TPL = [];
+ const Names = Object.keys(_Accessories);
+ for (let i = 0; i < Names.length; i++) {
+ const PL = {
+ "id": Names[i],
+ "type": _Accessories[Names[i]].getAccessoryType(),
+ "name": _Accessories[Names[i]].getAccessory().displayName,
+ "characteristics": _Accessories[Names[i]].getProperties()
+ }
+ TPL.push(PL)
+ }
+ res.contentType("application/json");
+ res.send(JSON.stringify(TPL));
+ }
+
+ // get Accessory
+ function _processAccessoryGet(req, res) {
+ const PW = CRYPTO.createHash('md5').update(req.params.pwd).digest("hex");
+ if (PW != CONFIG.loginPassword) {
+ res.sendStatus(401);
+ return;
+ }
+
+ const Ac = _Accessories[req.params.id]
+
+ if (Ac == null) {
+ res.contentType("application/json");
+ res.send(JSON.stringify({
+ "Error": "Device not found"
+ }));
+ return;
+ }
+
+ const PL = {
+ "id": req.params.id,
+ "type": Ac.getAccessoryType(),
+ "name": Ac.getAccessory().displayName,
+ "characteristics": Ac.getProperties()
+ }
+ res.contentType("application/json");
+ res.send(JSON.stringify(PL));
+ }
+
+ // Set Accessory data
+ function _processAccessorySet(req, res) {
+ const PW = CRYPTO.createHash('md5').update(req.params.pwd).digest("hex");
+ if (PW != CONFIG.loginPassword) {
+ res.sendStatus(401);
+ return;
+ }
+
+ const Ac = _Accessories[req.params.id]
+
+ if (Ac == null) {
+ res.contentType("application/json");
+ res.send(JSON.stringify({
+ "ok": false,
+ "reason": "Device not found"
+ }));
+ return;
+ }
+
+ Ac.setCharacteristics(req.body)
+ res.contentType("application/json");
+ res.send(JSON.stringify({
+ ok: true
+ }));
+ }
+
+ // Set Pair Status
+ this.setBridgePaired = function(IsPaired) {
+ _Paired = IsPaired;
+ }
+}
+
+module.exports = {
+ Server: Server
+}
\ No newline at end of file
diff --git a/core/util.js b/core/util.js
new file mode 100755
index 0000000..2a6435c
--- /dev/null
+++ b/core/util.js
@@ -0,0 +1,321 @@
+'use strict'
+const FS = require('fs');
+const PATH = require('path');
+const READLINE = require("readline");
+const CHALK = require('chalk');
+const CRYPTO = require('crypto')
+const OS = require('os');
+const ROOTPATH = PATH.join(OS.homedir(), "HKDS");
+const CONFIGPATH = PATH.join(ROOTPATH, "hkds_config.json");
+const HOMEKITPATH = PATH.join(ROOTPATH, "HomeKitPersist");
+const PKG = require('../package.json');
+const CACHEPATH = PATH.join(ROOTPATH, "characteristic_cache.json");
+const ROUTING = require("./routing");
+
+const RestoreMin = "4.0.0";
+const RestoreMax = "4.0.0";
+
+
+
+const saveCharacteristicCache = function (Cache) {
+ FS.writeFileSync(CACHEPATH, JSON.stringify(Cache), 'utf8', function (err) {
+ if (err) {
+ console.log(" Could not right to the config file.");
+ }
+ })
+}
+
+const getCharacteristicCache = function () {
+ if (FS.existsSync(CACHEPATH)) {
+ const C = FS.readFileSync(CACHEPATH, 'utf8');
+ return JSON.parse(C);
+ }
+
+ return null;
+}
+
+const restore = function (data) {
+ try {
+ let buff = Buffer.from(data.content, 'base64');
+ let text = buff.toString('utf8');
+ const JS = JSON.parse(text);
+
+ if (JS.sourceVersion < RestoreMin || JS.sourceVersion > RestoreMax) {
+ return "Version"
+ } else {
+ // Delete, restore config
+ FS.unlinkSync(CONFIGPATH);
+ saveConfig(JS.config);
+
+ // Delete Cache Files
+ FS.readdirSync(HOMEKITPATH).forEach(function (file, index) {
+ FS.unlinkSync(PATH.join(HOMEKITPATH, file));
+ });
+
+ // Restore Cache Files
+ for (let i = 0; i < JS.homekitCache.length; i++) {
+ FS.writeFileSync(PATH.join(HOMEKITPATH, JS.homekitCache[i].file), JSON.stringify(JS.homekitCache[i].content), 'utf8', function (err) {
+ if (err) {
+ console.log(" Could not right to cache file.");
+ process.exit(0);
+ }
+ })
+ }
+
+ return true;
+
+ }
+ } catch (err) {
+ return "Invalid"
+ }
+
+}
+
+const generateBackup = function () {
+ let BU = {};
+
+ // Min Version
+ BU.sourceVersion = PKG.version;
+
+ // Config
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ BU.config = ConfigOBJ
+
+ // Cache
+ BU.homekitCache = []
+ FS.readdirSync(HOMEKITPATH).forEach(function (file, index) {
+ const FC = FS.readFileSync(PATH.join(HOMEKITPATH, file), 'utf8');
+ const FCP = JSON.parse(FC);
+ BU.homekitCache.push({
+ "file": file,
+ "content": FCP
+ });
+ });
+
+ const JS = JSON.stringify(BU);
+
+ return Buffer.from(JS).toString('base64');
+
+}
+
+// new install check
+const checkNewEV = function () {
+
+ if (!FS.existsSync(CONFIGPATH)) {
+ reset();
+ }
+}
+
+const getRndInteger = function (min, max) {
+ return Math.floor(Math.random() * (max - min)) + min;
+}
+
+const genMAC = function () {
+ var hexDigits = "0123456789ABCDEF";
+ var macAddress = "";
+ for (var i = 0; i < 6; i++) {
+ macAddress += hexDigits.charAt(Math.round(Math.random() * 15));
+ macAddress += hexDigits.charAt(Math.round(Math.random() * 15));
+ if (i != 5) macAddress += ":";
+ }
+ return macAddress;
+}
+
+const makeID = function (length) {
+ var result = '';
+ var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ var charactersLength = characters.length;
+ for (var i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+ return result;
+}
+
+// Flash Route COnfig
+const updateRouteConfig = function (Config) {
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ ConfigOBJ.routes = Config;
+ saveConfig(ConfigOBJ);
+}
+
+// Adjust options
+const updateOptions = function (Config) {
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+
+ const ConfigOBJ = JSON.parse(CFF);
+
+ ConfigOBJ.enableIncomingMQTT = Config.enableIncomingMQTT;
+ ConfigOBJ.MQTTBroker = Config.MQTTBroker;
+ ConfigOBJ.MQTTTopic = Config.MQTTTopic;
+ ConfigOBJ.advertiser = Config.advertiser;
+ ConfigOBJ.interface = Config.interface;
+ ConfigOBJ.webInterfaceAddress = Config.webInterfaceAddress;
+ ConfigOBJ.webInterfacePort = Config.webInterfacePort;
+
+ if (ConfigOBJ.hasOwnProperty("MQTTOptions")) {
+ ConfigOBJ.MQTTOptions.username = Config.MQTTOptions.username
+ ConfigOBJ.MQTTOptions.password = Config.MQTTOptions.password
+ } else {
+ ConfigOBJ.MQTTOptions = {}
+ ConfigOBJ.MQTTOptions.username = Config.MQTTOptions.username
+ ConfigOBJ.MQTTOptions.password = Config.MQTTOptions.password
+ }
+
+ saveConfig(ConfigOBJ);
+}
+
+// Add Accessory
+const appendAccessoryToConfig = function (Accessory) {
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ ConfigOBJ.accessories.push(Accessory);
+ saveConfig(ConfigOBJ);
+}
+
+// Update Device Route
+const routeAccessory = function (AccessoryID, RouteName) {
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ const TargetAc = ConfigOBJ.accessories.filter(a => a.username.replace(/:/g, "") == AccessoryID)[0]
+ TargetAc.route = RouteName;
+ saveConfig(ConfigOBJ);
+}
+
+// Save new bridge
+const saveBridgeConfig = function (Config) {
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ ConfigOBJ.bridgeConfig = Config;
+ saveConfig(ConfigOBJ);
+}
+
+// Global write CFG
+const saveConfig = function (Config) {
+ FS.writeFileSync(CONFIGPATH, JSON.stringify(Config), 'utf8', function (err) {
+ if (err) {
+ console.log(" Could not right to the config file.");
+ process.exit(0);
+ }
+ })
+}
+
+// check password reset request
+const checkPassword = function () {
+ if (process.argv.length > 3) {
+ if (process.argv[2] == "passwd") {
+ const NPWD = process.argv[3];
+ const PW = CRYPTO.createHash('md5').update(NPWD).digest("hex");
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ ConfigOBJ.loginPassword = PW;
+ saveConfig(ConfigOBJ);
+ console.log(CHALK.keyword('yellow')(" Password has been set."))
+ console.log('')
+ process.exit(0);
+ }
+ }
+}
+
+// check password reset request
+const checkInstallRequest = function () {
+ if (process.argv.length > 3) {
+ if (process.argv[2] == "installmodule") {
+ const Module = process.argv[3];
+
+ ROUTING.install(Module)
+ console.log(CHALK.keyword('green')(" Module ["+Module+"] has been installed."))
+ console.log('')
+ process.exit(0);
+
+ }
+ }
+}
+
+// Delete Accessory
+const deleteAccessory = function (AccessoryID) {
+ const CFF = FS.readFileSync(CONFIGPATH, 'utf8');
+ const ConfigOBJ = JSON.parse(CFF);
+ const NA = ConfigOBJ.accessories.filter(a => a.username.replace(/:/g, "") != AccessoryID)
+ ConfigOBJ.accessories = NA;
+ saveConfig(ConfigOBJ);
+}
+
+// check reset request
+const checkReset = function () {
+ if (process.argv.length > 2) {
+ if (process.argv[2] == "reset") {
+ const rl = READLINE.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+ console.log(CHALK.keyword('yellow')(" -- WARNING --"))
+ console.log('')
+ console.log(CHALK.keyword('yellow')(" HomeKit Device Stack is about to be RESET!!."))
+ console.log(CHALK.keyword('yellow')(" This will."))
+ console.log('')
+ console.log(CHALK.keyword('yellow')(" - Delete all your Accessories (Including any CCTV Cameras)."))
+ console.log(CHALK.keyword('yellow')(" - Destroy the Bridge hosting those Accessories."))
+ console.log(CHALK.keyword('yellow')(" - Delete all HomeKit cache data."))
+ console.log(CHALK.keyword('yellow')(" - Delete all HomeKit Device Stack Configuration."))
+ console.log(CHALK.keyword('yellow')(" - Discard any Accessory identification."))
+ console.log(CHALK.keyword('yellow')(" - Reset the login details for the UI."))
+ console.log('')
+ console.log(CHALK.keyword('yellow')(" Evan if you recreate Accessories, you will need to re-enroll HomeKit Device Stack on your iOS device."))
+ console.log('')
+ rl.question(" Continue? (y/n) :: ", function (value) {
+ if (value.toUpperCase() == 'Y') {
+ console.log('')
+ reset();
+ console.log(' Homekit Device Stack has been reset.');
+ console.log('')
+ process.exit(0);
+ } else {
+ process.exit(0);
+ }
+ });
+ return true
+ } else {
+ return false;
+ }
+ }
+}
+
+// The acrtual reset script
+const reset = function () {
+
+ FS.rmdirSync(ROOTPATH, { recursive: true })
+ FS.mkdirSync(ROOTPATH, { recursive: true })
+
+ let DefaultFile = PATH.join(process.cwd(),"hkds_config.json.default")
+ let SaveTo = PATH.join(ROOTPATH,"hkds_config.json");
+
+ FS.copyFileSync(DefaultFile,SaveTo)
+
+
+}
+
+module.exports = {
+ getRndInteger: getRndInteger,
+ genMAC: genMAC,
+ makeID: makeID,
+ routeAccessory: routeAccessory,
+ saveConfig: saveConfig,
+ appendAccessoryToConfig: appendAccessoryToConfig,
+ checkReset: checkReset,
+ saveBridgeConfig: saveBridgeConfig,
+ updateRouteConfig: updateRouteConfig,
+ checkPassword: checkPassword,
+ deleteAccessory: deleteAccessory,
+ updateOptions: updateOptions,
+ ConfigPath: CONFIGPATH,
+ HomeKitPath: HOMEKITPATH,
+ RootPath: ROOTPATH,
+ checkNewEV: checkNewEV,
+ generateBackup: generateBackup,
+ restore: restore,
+ saveCharacteristicCache: saveCharacteristicCache,
+ getCharacteristicCache: getCharacteristicCache,
+ checkInstallRequest:checkInstallRequest
+}
\ No newline at end of file
diff --git a/hkds_config.json.default b/hkds_config.json.default
new file mode 100644
index 0000000..a3b530b
--- /dev/null
+++ b/hkds_config.json.default
@@ -0,0 +1,47 @@
+{
+ "advertiser": "ciao",
+ "interface": "ALL",
+ "loginUsername": "admin",
+ "loginPassword": "21232f297a57a5a743894a0e4a801fc3",
+ "webInterfacePort": 7989,
+ "webInterfaceAddress": "ALL",
+ "enableIncomingMQTT": "false",
+ "MQTTBroker": "mqtt://test.mosquitto.org",
+ "MQTTTopic": "HKDS/IN/+",
+ "MQTTOptions": {
+ "username": "",
+ "password": ""
+ },
+ "bridgeConfig": {
+ },
+ "routes": {
+ "Node Red": {
+ "type": "hkds-route-http",
+ "destinationURI": "http://192.168.0.2:1880/HKDS/{{accessoryID}}"
+ },
+ "Output To Console": {
+ "type": "hkds-route-console"
+ },
+ "UDP Broadcast": {
+ "type": "hkds-route-udp",
+ "address": "255.255.255.255",
+ "port": 34322
+ },
+ "File Output": {
+ "type": "hkds-route-file",
+ "directory": "/accessory-events/{{accessoryID}}"
+ },
+ "MQTT Broker": {
+ "type": "hkds-route-mqtt",
+ "mqttbroker":"mqtt://broker.mqttdashboard.com",
+ "mqttusername":"",
+ "mqttpassword":"",
+ "topic": "HKDS/OUT/{{accessoryID}}"
+ },
+ "Websocket": {
+ "type": "hkds-route-websocket",
+ "uri": "ws://server.com:9999/path/to/websocket"
+ }
+ },
+ "accessories": []
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100755
index 0000000..af6efee
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "homekit-device-stack",
+ "version": "4.0.0",
+ "description": "A Middleware Server, for bringing homekit functionality to your Home Automation.",
+ "main": "App.js",
+ "keywords": [
+ "homekit",
+ "smart",
+ "home",
+ "automation",
+ "hap",
+ "apple",
+ "ios",
+ "accessory",
+ "virtual",
+ "hap-nodejs",
+ "device"
+ ],
+ "author": {
+ "name": "Marcus Davies",
+ "email": "marcus.davies83@icloud.com"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "chalk":"4.1.0",
+ "child_process":"1.0.2",
+ "cookie-parser":"1.4.5",
+ "events":"3.3.0",
+ "express":"4.17.1",
+ "hap-nodejs":"0.9.4",
+ "ip":"1.1.5",
+ "mqtt":"4.2.6",
+ "handlebars": "4.7.7",
+ "readline":"1.3.0",
+ "node-cleanup": "2.1.2"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/marcus-j-davies/Homekit-Device-Stack.git"
+ },
+ "bugs": {
+ "url": "https://github.com/marcus-j-davies/Homekit-Device-Stack/issues"
+ },
+ "homepage": "https://github.com/marcus-j-davies/Homekit-Device-Stack#readme"
+}
diff --git a/ui/create.tpl b/ui/create.tpl
new file mode 100644
index 0000000..79e0f0c
--- /dev/null
+++ b/ui/create.tpl
@@ -0,0 +1,86 @@
+
+
+
+
+ Homekit Device Stack
+
+
+
+
+
+
+
+
+
+
+
Homekit Device Stack
+
+
+
+
+ Create New Accessory
+
+ Cancel
+
+
+
+
+
+
+
+
+
+ Once created, the accessory should appear in
+ HomeApp within a few moments. Closing HomeApp and opening it again maybe required in some cases.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Enroll the new device
+
+
+ You do this in the same way you enrolled HomeKit Device Stack.
+
+
On your iOS device open the 'Home' app
+
Click the '+' button (top right) and choose 'Add Accessory'
+
Scan this QR Code
+
The Home app will guide you through in setting up this device.
+
+ Once enrolled, you will be taken back to your devices.
+ If you have problems in using the QR Code, click 'I Don't have a Code or Cannot Scan' you will see the device you just created, click it, and you will be asked for a pin, enter the pin code displayed here.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/edit.tpl b/ui/edit.tpl
new file mode 100644
index 0000000..da01990
--- /dev/null
+++ b/ui/edit.tpl
@@ -0,0 +1,57 @@
+
+
+
+
+ Homekit Device Stack
+
+
+
+
+
+
+
+
+
+
+
Homekit Device Stack
+
+
+
+
+ Editing Accessory
+
+ Cancel
+
+
+
+
+
+
+
+
+
+ Changes to the accessory should take affect in a few moments.
+ The accessory may, for a short while, disappear from HomeApp.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/login.tpl b/ui/login.tpl
new file mode 100644
index 0000000..0fcd6ac
--- /dev/null
+++ b/ui/login.tpl
@@ -0,0 +1,46 @@
+
+
+
+
+
+ Homekit Device Stack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Homekit Device Stack.
+ Version: 4.0.0
+ Homekit Device Stack, is not affiliated with, or endoresed by Apple Inc.
+
+
+
+
+
Username
+
+
+
+
Password
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/main.-OLD.tpl b/ui/main.-OLD.tpl
new file mode 100644
index 0000000..23ed76c
--- /dev/null
+++ b/ui/main.-OLD.tpl
@@ -0,0 +1,341 @@
+
+
+
+
+ Homekit Device Stack
+
+
+
+
+
+
+
+
+
+
+
Homekit Device Stack
+
+ Settings
+
+
+
+
+
+ Devices (Double click to edit)
+
+ Create New
+ Accessory
+
+
+ {{#Config.accessories}}
+
+
+
+
+
+ {{name}}
+ Device ID : {{accessoryID}}
+ Serial ID : {{serialNumber}}
+
+ When your accessories change state, HomeKit Devices Stack needs to know where to send the change event.
+ This is called a 'Route'. Once a Route has been created, you will be able to 'plumb' your devices into this Route.
+
+
+
+
+
+
Route Module
+
+
+
+
+
+
Route Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Accessory Type
+
+ Cancel
+
+
+ {{#AccessoryTypes}}
+
+
+ {{Label}}
+
+ {{/AccessoryTypes}}
+
+
+
+
+
+
+ Homekit Device Stack Settings
+
+ Close
+
+
+
+
+
+
+
HomeKit Device Stack
Please note : The options below will require a restart of HomeKit Device Stack.
+
+
+
+
Enable MQTT Client
+
+
+
+
Broker
+
+
+
+
Topic
+
+
+
+
Username
+
+
+
+
Password
+
+
+
+
 
+
 
+
+
+
Web Interface/REST API Interface
+
+
+
+
+
+
Web Interface/REST API Port
+
+
+
+
 
+
 
+
+
+
mDNS Advertiser
+
+
+
+
+
+
Bind To Interface
+
+
+
+
+
+
 
+
 
+
+
+
Save Changes (Requires Restart)
+
+
+
+
 
+
 
+
+
+
Backup Configuration and Homekit Data
+
+
+
+
Restore Configuration and Homekit Data
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/main.tpl b/ui/main.tpl
new file mode 100644
index 0000000..a46dc99
--- /dev/null
+++ b/ui/main.tpl
@@ -0,0 +1,38 @@
+
+
+
+
+
+ Homekit Device Stack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Accessories
+
Routing
+
Settings
+
Bridge
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/setup.tpl b/ui/setup.tpl
new file mode 100644
index 0000000..3ff6b45
--- /dev/null
+++ b/ui/setup.tpl
@@ -0,0 +1,64 @@
+
+
+
+
+ Homekit Device Stack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
HomeKit Device Stack Accessory Bridge
+
+
+
+ HomeKit Device Stack can act as a HomeKit Bridge. That is, its a kind of hub that has smart
+ accessories attached. you can publish your devices either attached to, or separate from the bridge.
+
+
+
On your iOS device open the 'Home' app
+
Click the '+' button (top right) and choose 'Add Accessory'
+
Scan this QR Code
+
The Home app will guide you through in setting up a Test Device (Switch
+ Accessory Demo) which is attached to the bridge.
+
+
+ Once paired, your devices will be listed here. If you have problems in using the QR
+ Code, click 'I Don't have a Code or Cannot Scan' you will see 'HomeKit Device Stack',
+ click it, and you will be asked for a pin, enter the pin code displayed here.
+
+
+
+
+
+ {{Config.bridgeConfig.pincode}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/static/Images/BG.png b/ui/static/Images/BG.png
new file mode 100644
index 0000000..7a6d737
Binary files /dev/null and b/ui/static/Images/BG.png differ
diff --git a/ui/static/Images/HKDS.png b/ui/static/Images/HKDS.png
new file mode 100644
index 0000000..6b6993d
Binary files /dev/null and b/ui/static/Images/HKDS.png differ
diff --git a/ui/static/Images/Logo.png b/ui/static/Images/Logo.png
new file mode 100644
index 0000000..ad48cea
Binary files /dev/null and b/ui/static/Images/Logo.png differ
diff --git a/ui/static/Images/device_icons/Ac_ALARM.png b/ui/static/Images/device_icons/Ac_ALARM.png
new file mode 100644
index 0000000..95bfbb2
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_ALARM.png differ
diff --git a/ui/static/Images/device_icons/Ac_CAMERA.png b/ui/static/Images/device_icons/Ac_CAMERA.png
new file mode 100644
index 0000000..909f4f3
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_CAMERA.png differ
diff --git a/ui/static/Images/device_icons/Ac_CONTACT_SENSOR.png b/ui/static/Images/device_icons/Ac_CONTACT_SENSOR.png
new file mode 100644
index 0000000..1c5a0d0
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_CONTACT_SENSOR.png differ
diff --git a/ui/static/Images/device_icons/Ac_FAN.png b/ui/static/Images/device_icons/Ac_FAN.png
new file mode 100644
index 0000000..1459aec
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_FAN.png differ
diff --git a/ui/static/Images/device_icons/Ac_GARAGE_DOOR.png b/ui/static/Images/device_icons/Ac_GARAGE_DOOR.png
new file mode 100644
index 0000000..00da7af
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_GARAGE_DOOR.png differ
diff --git a/ui/static/Images/device_icons/Ac_LIGHTBULB.png b/ui/static/Images/device_icons/Ac_LIGHTBULB.png
new file mode 100644
index 0000000..6814fd5
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_LIGHTBULB.png differ
diff --git a/ui/static/Images/device_icons/Ac_LOCK.png b/ui/static/Images/device_icons/Ac_LOCK.png
new file mode 100644
index 0000000..0cf9ec6
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_LOCK.png differ
diff --git a/ui/static/Images/device_icons/Ac_Leak.png b/ui/static/Images/device_icons/Ac_Leak.png
new file mode 100644
index 0000000..f5f622b
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_Leak.png differ
diff --git a/ui/static/Images/device_icons/Ac_LightSensor.png b/ui/static/Images/device_icons/Ac_LightSensor.png
new file mode 100644
index 0000000..437cb8a
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_LightSensor.png differ
diff --git a/ui/static/Images/device_icons/Ac_MOTION_SENSOR.png b/ui/static/Images/device_icons/Ac_MOTION_SENSOR.png
new file mode 100644
index 0000000..a7eca6d
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_MOTION_SENSOR.png differ
diff --git a/ui/static/Images/device_icons/Ac_OUTLET.png b/ui/static/Images/device_icons/Ac_OUTLET.png
new file mode 100755
index 0000000..81d3599
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_OUTLET.png differ
diff --git a/ui/static/Images/device_icons/Ac_SWITCH.png b/ui/static/Images/device_icons/Ac_SWITCH.png
new file mode 100755
index 0000000..1248736
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_SWITCH.png differ
diff --git a/ui/static/Images/device_icons/Ac_Smoke.png b/ui/static/Images/device_icons/Ac_Smoke.png
new file mode 100644
index 0000000..17ec7a7
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_Smoke.png differ
diff --git a/ui/static/Images/device_icons/Ac_THERMOSTAT.png b/ui/static/Images/device_icons/Ac_THERMOSTAT.png
new file mode 100644
index 0000000..4f21508
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_THERMOSTAT.png differ
diff --git a/ui/static/Images/device_icons/Ac_TV.png b/ui/static/Images/device_icons/Ac_TV.png
new file mode 100755
index 0000000..c24eeba
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_TV.png differ
diff --git a/ui/static/Images/device_icons/Ac_Temp.png b/ui/static/Images/device_icons/Ac_Temp.png
new file mode 100644
index 0000000..c6c02dd
Binary files /dev/null and b/ui/static/Images/device_icons/Ac_Temp.png differ
diff --git a/ui/static/Images/route_icons/FILE.png b/ui/static/Images/route_icons/FILE.png
new file mode 100644
index 0000000..8620d00
Binary files /dev/null and b/ui/static/Images/route_icons/FILE.png differ
diff --git a/ui/static/Images/route_icons/HTTP.png b/ui/static/Images/route_icons/HTTP.png
new file mode 100644
index 0000000..97f08f1
Binary files /dev/null and b/ui/static/Images/route_icons/HTTP.png differ
diff --git a/ui/static/Images/route_icons/MQTT.png b/ui/static/Images/route_icons/MQTT.png
new file mode 100644
index 0000000..0269f28
Binary files /dev/null and b/ui/static/Images/route_icons/MQTT.png differ
diff --git a/ui/static/Images/route_icons/UDP.png b/ui/static/Images/route_icons/UDP.png
new file mode 100644
index 0000000..58107b3
Binary files /dev/null and b/ui/static/Images/route_icons/UDP.png differ
diff --git a/ui/static/Images/route_icons/WEBSOCKET.png b/ui/static/Images/route_icons/WEBSOCKET.png
new file mode 100644
index 0000000..cb64ce5
Binary files /dev/null and b/ui/static/Images/route_icons/WEBSOCKET.png differ
diff --git a/ui/static/JS/functions.js b/ui/static/JS/functions.js
new file mode 100755
index 0000000..7fcf685
--- /dev/null
+++ b/ui/static/JS/functions.js
@@ -0,0 +1,549 @@
+let EndPoints = []
+let Connectors = []
+
+// Login
+function Login() {
+ let Data = {
+ "username": $('#TXT_Username').val(),
+ "password": $('#TXT_Password').val(),
+ }
+ $.ajax({
+ type: "POST",
+ url: "../../../ui/login",
+ data: JSON.stringify(Data),
+ contentType: "application/json; charset=utf-8",
+ dataType: "json",
+ success: LoginDone
+ });
+
+}
+
+function LoginDone(data) {
+ if (data.success) {
+ document.location = data.destination;
+ } else {
+ $('#Message').text('Invalid username and/or password');
+ }
+}
+
+// delete route()
+function DeleteRoute() {
+ if (confirm("Are you sure you wish to delete this route?")) {
+ let Name = $("#RDelete").attr("route_name");
+
+ $.ajax({
+ type: "POST",
+ data: JSON.stringify({
+ "name": "" + Name + ""
+ }),
+ contentType: "application/json",
+ url: "../../../ui/deleteroute",
+ dataType: "json",
+ success: ByPass
+ });
+ }
+}
+
+// save config
+function SaveConfig() {
+ let Data = {
+ "webInterfacePort": parseInt($('#CFG_Port').val()),
+ "advertiser": $('#CFG_Advertiser').val(),
+ "MQTTBroker": $('#CFG_Broker').val(),
+ "MQTTTopic": $('#CFG_Topic').val(),
+ "MQTTOptions": {
+ "username": "" + $('#CFG_username').val() + "",
+ "password": "" + $('#CFG_password').val() + ""
+ },
+ "enableIncomingMQTT": "" + ($('#CFG_MQTT').prop("checked") == true) + "",
+ "interface": $("#CFG_Interface").val(),
+ "webInterfaceAddress": $("#CFG_Address").val()
+ }
+
+ $.ajax({
+ type: "POST",
+ url: "../../../ui/setconfig",
+ data: JSON.stringify(Data),
+ contentType: "application/json; charset=utf-8",
+ dataType: "json",
+ success: function() {
+ alert('Please restart the server to apply the new configuration.')
+ }
+ });
+}
+
+// Restore
+function StartRestore() {
+ if (confirm('Are you sure you wish to restore your configuration. This will include all Homekit client mappings?')) {
+ let IP = document.createElement("input");
+ IP.setAttribute("type", "file");
+ IP.onchange = ProcessRestore;
+ IP.click();
+
+ }
+}
+
+function ProcessRestore(input) {
+ let FR = new FileReader();
+
+ FR.onload = function() {
+ $.ajax({
+ type: "POST",
+ data: JSON.stringify({
+ "content": "" + FR.result + ""
+ }),
+ contentType: "application/json",
+ url: "../../../ui/restore",
+ dataType: "json",
+ success: RestoreDone
+ });
+
+ }
+ FR.readAsText(input.target.files[0]);
+
+}
+
+function RestoreDone(data) {
+ if (data.OK) {
+ alert('Configuration Restored! The server has been shutdown - please start it again to apply the Configuration.')
+ } else {
+ if (data.Reason == "VersionMismatch") {
+ alert('This backup is not compatible with the version of HomeKit Devcie Stack Installed.')
+ } else {
+ alert('The backup is invalid.')
+ }
+
+ }
+}
+
+// Device Setup
+function StartPairCheck(ID) {
+ setInterval(() => {
+ DoCheckDevice(ID)
+ }, 5000);
+}
+
+function DoCheckDevice(ID) {
+ $.ajax({
+ type: "GET",
+ url: "../../../ui/pairstatus/" + ID,
+ dataType: "json",
+ success: Check
+ });
+}
+
+// Bridge Setup
+function StartIntervalCheck() {
+ setInterval(DoCheck, 5000);
+}
+
+function DoCheck() {
+ $.ajax({
+ type: "GET",
+ url: "../../../ui/pairstatus",
+ dataType: "json",
+ success: Check
+ });
+}
+
+function Check(data) {
+ if (data.paired) {
+ document.location = '../../../ui/main'
+ }
+}
+
+// Root Type Change
+function RouteTypeChanged() {
+
+ let Type = $('#R_Type').val();
+
+ $.ajax({
+
+ type: "GET",
+ url: "../../../ui/getroutemeta/"+Type,
+ dataType: "json",
+ success: function(data){
+
+ let Anchor = $("#Anchor")
+
+ $(".param_row").remove()
+
+ data.Inputs.reverse()
+
+ data.Inputs.forEach((I) =>{
+
+
+ Anchor.after('