Skip to content

Commit

Permalink
Merge pull request #8 from domapic/add-stateless-switch-accesory
Browse files Browse the repository at this point in the history
Add stateless switch accesory
  • Loading branch information
javierbrea committed Dec 9, 2018
2 parents 1b2913c + 31d267f commit 19113fb
Show file tree
Hide file tree
Showing 18 changed files with 442 additions and 65 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
### Removed

## [1.0.0-beta.2] - 2018-12-09
### Added
- Add Button accessory. Register abilities with action and no data type defined as Buttons

### Changed
- Set Homebridge server name from service name
- Set Homebridge server random mac, unique by plugin instance
- Upgraded domapic-service dependency

### Fixed
- Write homebridge config in domapic storage folder instead of a child of package folder

## [1.0.0-beta.1] - 2018-12-07
### Added
- First prerelease
Expand Down
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -18,7 +18,10 @@ Since Siri supports devices added through HomeKit, this means that **with this p
* _"Siri, open the garage door"_
* _"Siri, activate my awesome webhook"_

> For now, only abilities which have "boolean" data type and have both `state` and `action` are being exposed as HomeKit `Switch` accessories. Soon will be added custom plugin configuration for abilities to [Domapic Controller][domapic-controller-url], and then the user will be able to decide which type of accessory should be each ability, as long as data type is compatible.
> For now, only certain types of abilities are being registered as accessories:
- Abilities that have "boolean" data type and have both `state` and `action` are being exposed as HomeKit `Switch` accesories.
- Abilities without data type that have an `action` defined are being exposed as Buttons. (HomeKit Switch returning always `false` as state).
Soon will be added custom plugin configuration for abilities to [Domapic Controller][domapic-controller-url], and then the user will be able to decide which type of accessory should be each ability, as long as data type is compatible.

## Prerequisites

Expand Down
4 changes: 3 additions & 1 deletion lib/Homebridge.js
Expand Up @@ -18,6 +18,7 @@ const {

class HomeBridge {
constructor (dpmcPlugin) {
this.plugin = dpmcPlugin
this.tracer = dpmcPlugin.tracer
this.process = null
this.binPath = path.resolve(PACKAGE_PATH, 'node_modules', '.bin', 'homebridge')
Expand Down Expand Up @@ -46,9 +47,10 @@ class HomeBridge {
async start () {
await this.tracer.info(HOMEBRIDGE_STARTING)
await this.writePackageJson()
const homebridgePath = path.resolve(await this.plugin.storage.getPath(), HOMEBRIDGE_PATH)
this.process = childProcess.spawn(this.binPath, [
'-U',
HOMEBRIDGE_PATH,
homebridgePath,
'-P',
this.pluginPath
], {
Expand Down
69 changes: 50 additions & 19 deletions lib/HomebridgeConfig.js
Expand Up @@ -4,21 +4,23 @@ const path = require('path')

const fsExtra = require('fs-extra')
const ip = require('ip')
const { cloneDeep } = require('lodash')
const { cloneDeep, toUpper } = require('lodash')
const randomMac = require('random-mac')

const homebridgeConfig = require('./homebridge-config.json')

const {
HOMEBRIDGE_PATH,
WRITING_HOMEBRIDGE_CONFIG,
HOMEBRIDGE_PORT,
HOMEBRIDGE_MAC_STORAGE_KEY,

ACCESORY_SWITCH_NAME
ACCESORY_SWITCH_NAME,
ACCESORY_BUTTON_NAME
} = require('./statics')

class Accesories {
constructor (dpmcPlugin) {
this.configPath = path.resolve(HOMEBRIDGE_PATH, 'config.json')
this.plugin = dpmcPlugin
this.tracer = dpmcPlugin.tracer
}
Expand All @@ -36,36 +38,65 @@ class Accesories {
}
}

getAccessoryConfig (accesoryName, ability, pluginConnection) {
return {
accessory: accesoryName,
apiKey: pluginConnection.apiKey,
bridgeUrl: `${pluginConnection.url}${ability._id}`,
servicePackageName: ability.service.package,
serviceName: ability.service.name,
serviceProcessId: ability.service.processId,
abilityName: ability.name,
name: `${ability.service.name} ${ability.name}`
}
}

getSwitchs (abilities, pluginConnection) {
// Abilities with boolean data that have state and action can be considered as homebridge "switches"
const switchValidAbilities = abilities.filter(ability => ability.type === 'boolean' && ability.state === true && ability.action === true)
return switchValidAbilities.map(ability => {
return {
accessory: ACCESORY_SWITCH_NAME,
apiKey: pluginConnection.apiKey,
bridgeUrl: `${pluginConnection.url}${ability._id}`,
servicePackageName: ability.service.package,
serviceName: ability.service.name,
serviceProcessId: ability.service.processId,
abilityName: ability.name,
name: `${ability.service.name} ${ability.name}`
}
})
const validAbilities = abilities.filter(ability => ability.type === 'boolean' && ability.state === true && ability.action === true)
return validAbilities.map(ability => this.getAccessoryConfig(ACCESORY_SWITCH_NAME, ability, pluginConnection))
}

getButtons (abilities, pluginConnection) {
// Abilities with no defined data that have an action can be considered as Domapic "Buttons", that will be mapped to homebridge "switches". Will return false as state always
const validAbilities = abilities.filter(ability => ability.type === undefined && ability.action === true)
return validAbilities.map(ability => this.getAccessoryConfig(ACCESORY_BUTTON_NAME, ability, pluginConnection))
}

async getAccesories (abilities) {
const pluginConnection = await this.getPluginConnection()
return [
...this.getSwitchs(abilities, pluginConnection)
...this.getSwitchs(abilities, pluginConnection),
...this.getButtons(abilities, pluginConnection)
]
}

async getMac () {
let mac
try {
mac = await this.plugin.storage.get(HOMEBRIDGE_MAC_STORAGE_KEY)
} catch (err) {
mac = toUpper(randomMac())
await this.plugin.storage.set(HOMEBRIDGE_MAC_STORAGE_KEY, mac)
}
return mac
}

async write (abilities) {
await this.tracer.info(WRITING_HOMEBRIDGE_CONFIG)
const pluginConfig = await this.plugin.config.get()
const homebridgePath = path.resolve(await this.plugin.storage.getPath(), HOMEBRIDGE_PATH)

this.config = cloneDeep(homebridgeConfig)
this.config.bridge.port = await this.plugin.config.get(HOMEBRIDGE_PORT)

this.config.bridge.name = pluginConfig.name
this.config.bridge.username = await this.getMac()
this.config.bridge.port = pluginConfig[HOMEBRIDGE_PORT]
this.config.accessories = await this.getAccesories(abilities)
await fsExtra.writeJson(this.configPath, this.config, {

fsExtra.ensureDirSync(homebridgePath)

await fsExtra.writeJson(path.resolve(homebridgePath, 'config.json'), this.config, {
spaces: 2
})
}
Expand Down
4 changes: 1 addition & 3 deletions lib/homebridge-config.json
@@ -1,9 +1,7 @@
{
"bridge": {
"name": "Homebridge",
"username": "CC:22:3D:E3:CE:30"
},
"description": "Homebridge server for Domapic Homekit integration",
"description": "Homebridge server for Domapic HomeKit integration",
"accessories": [
]
}
76 changes: 76 additions & 0 deletions lib/plugins/ButtonFactory.js
@@ -0,0 +1,76 @@
'use strict'

const url = require('url')

const requestPromise = require('request-promise')

const { DOMAPIC, HOMEBRIDGE_ERROR, ACCESORY_BUTTON_NAME } = require('../statics')

const ButtonFactory = function (Service, Characteristic) {
return class Button {
constructor (log, config) {
this.config = config
this.log = log
this.bridgeUrl = url.parse(this.config.bridgeUrl)
this.getServices = this.getServices.bind(this)
this.getSwitchOnCharacteristic = this.getSwitchOnCharacteristic.bind(this)
this.setSwitchOnCharacteristic = this.setSwitchOnCharacteristic.bind(this)

this.requestOptions = {
uri: this.bridgeUrl,
json: true,
headers: {
'X-Api-Key': this.config.apiKey
},
method: 'GET'
}
}

static get name () {
return ACCESORY_BUTTON_NAME
}

logError (error) {
this.log(`${HOMEBRIDGE_ERROR} ${error.message}`)
}

getServices () {
const informationService = new Service.AccessoryInformation()
informationService
.setCharacteristic(Characteristic.Manufacturer, DOMAPIC)
.setCharacteristic(Characteristic.Model, this.config.servicePackageName)
.setCharacteristic(Characteristic.SerialNumber, this.config.serviceProcessId)

const switchService = new Service.Switch(this.config.name)
switchService.getCharacteristic(Characteristic.On)
.on('get', this.getSwitchOnCharacteristic)
.on('set', this.setSwitchOnCharacteristic)

this.informationService = informationService
this.switchService = switchService
return [informationService, switchService]
}

getSwitchOnCharacteristic (next) {
this.log(`Getting button state. Returning false`)
next(null, false)
return Promise.resolve()
}

setSwitchOnCharacteristic (on, next) {
this.log(`Activating button`)
return requestPromise({
...this.requestOptions,
method: 'POST'
}).then(() => {
this.log(`Button activation success`)
next()
}).catch(error => {
this.logError(error)
next(error)
})
}
}
}

module.exports = ButtonFactory
11 changes: 6 additions & 5 deletions lib/statics.js
Expand Up @@ -3,25 +3,26 @@
const path = require('path')

const PACKAGE_PATH = path.resolve(__dirname, '..')
const HOMEBRIDGE_PATH = path.resolve(PACKAGE_PATH, '.homebridge')

const DOMAPIC = 'Domapic'

module.exports = {
DOMAPIC,
LOADING_ABILITIES: 'Loading abilities from Controller',
WRITING_HOMEBRIDGE_CONFIG: 'Writing Homebridge configuration file',
HOMEBRIDGE_MAC_STORAGE_KEY: 'username_mac',
HOMEBRIDGE_LOG: '[Homebrigde log]',
HOMEBRIDGE_ERROR: 'ERROR:',
HOMEBRIDGE_EXITED: 'Homebridge exited with code',
HOMEBRIDGE_STOPPING: 'Stopping Homebridge server',
HOMEBRIDGE_STARTING: 'Starting Homebridge server',
HOMEBRIDGE_PATH: 'homebridge',
HOMEBRIDGE_PORT: 'homebridgePort',
PACKAGE_PATH,
HOMEBRIDGE_PATH,

ACCESORY_SWITCH_NAME: `${DOMAPIC}Switch`,

SENDING_ABILITY_ACTION: `Sending action to ability`,
GETTING_ABILITY_STATE: `Getting state of ability`
GETTING_ABILITY_STATE: `Getting state of ability`,

ACCESORY_SWITCH_NAME: `${DOMAPIC}Switch`,
ACCESORY_BUTTON_NAME: `${DOMAPIC}Button`
}
51 changes: 32 additions & 19 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 19113fb

Please sign in to comment.