Skip to content

Commit

Permalink
fix: more reliable connection with windows support
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `compositeTargets` is no longer supported.  You can achieve this same behavior by grouping accessories within the Home app
  • Loading branch information
dgreif committed Aug 7, 2021
1 parent 4a9452d commit 2f0e1f0
Show file tree
Hide file tree
Showing 8 changed files with 1,134 additions and 3,211 deletions.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"ignorePatterns": [
Expand Down
4,070 changes: 903 additions & 3,167 deletions package-lock.json

Large diffs are not rendered by default.

27 changes: 12 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,20 @@
"node": ">=12"
},
"devDependencies": {
"@types/node": "14.14.19",
"@typescript-eslint/eslint-plugin": "4.11.1",
"@typescript-eslint/parser": "4.11.1",
"@types/node": "16.4.13",
"@typescript-eslint/eslint-plugin": "4.29.0",
"@typescript-eslint/parser": "4.29.0",
"conventional-github-releaser": "3.1.5",
"dotenv": "8.2.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-prettier": "3.3.0",
"homebridge": "1.1.7",
"homebridge-config-ui-x": "4.36.0",
"prettier": "^2.2.1",
"standard-version": "9.1.0",
"typescript": "4.1.3"
},
"dependencies": {
"somfy-synergy": "^1.4.0"
"dotenv": "10.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.4.0",
"homebridge": "1.3.4",
"prettier": "^2.3.2",
"standard-version": "^9.3.1",
"typescript": "4.3.5"
},
"dependencies": {},
"files": [
"CHANGELOG.md",
"config.schema.json",
Expand Down
28 changes: 10 additions & 18 deletions src/accessory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { hap } from './hap'
import { SomfyMyLinkPlatform } from './platform'
import { MyLinkTargetConfig } from './config'
import {
AccessoryPlugin,
Expand All @@ -8,17 +7,16 @@ import {
CharacteristicValue,
Service as HapService,
} from 'homebridge'
import { logError, logInfo } from './util'
import { MyLink } from './my-link'

export class SomfyMyLinkTargetAccessory implements AccessoryPlugin {
public name = `Somfy ${this.target.name}`
public uuid_base = hap.uuid.generate(`mylink.target.${this.target.targetID}`)
private currentValue = 0
private services: HapService[] = []

constructor(
private platform: SomfyMyLinkPlatform,
private target: MyLinkTargetConfig
) {
constructor(private myLink: MyLink, private target: MyLinkTargetConfig) {
const { Service, Characteristic } = hap,
accessoryInformationService = new Service.AccessoryInformation()

Expand All @@ -38,7 +36,8 @@ export class SomfyMyLinkTargetAccessory implements AccessoryPlugin {
Characteristic.CurrentPosition
),
positionState = service.getCharacteristic(Characteristic.PositionState),
targetPosition = service.getCharacteristic(Characteristic.TargetPosition)
targetPosition = service.getCharacteristic(Characteristic.TargetPosition),
target = this.myLink.target(targetID)

targetPosition.on(
CharacteristicEventTypes.SET,
Expand All @@ -49,11 +48,8 @@ export class SomfyMyLinkTargetAccessory implements AccessoryPlugin {
try {
callback()

this.platform.log(
'Setting position of %s from %s to %s.',
`target ${targetID} (${name})`,
`${this.currentValue}%`,
`${targetValue}%`
logInfo(
`Setting position of target ${targetID} (${name}) from ${this.currentValue}% to ${targetValue}%.`
)

positionState.setValue(
Expand All @@ -64,8 +60,6 @@ export class SomfyMyLinkTargetAccessory implements AccessoryPlugin {
: Characteristic.PositionState.STOPPED
)

const target = this.platform.synergy.target(targetID)

if (targetValue === 0) {
await target[orientation.closed]()
} else if (targetValue === 100) {
Expand All @@ -78,10 +72,8 @@ export class SomfyMyLinkTargetAccessory implements AccessoryPlugin {
} catch (error) {
targetPosition.updateValue(this.currentValue)

this.platform.log(
'Encountered an error setting target position of %s: %s',
`target ${targetID} (${name})`,
error.message
logError(
`Encountered an error setting target position of target ${targetID} (${name}): ${error.message}`
)
} finally {
currentPosition.updateValue(this.currentValue)
Expand All @@ -91,7 +83,7 @@ export class SomfyMyLinkTargetAccessory implements AccessoryPlugin {
)

// Set a more sane default value for the current position.
this.currentValue = (currentPosition.getDefaultValue() as number) || 0
this.currentValue = 0
currentPosition.updateValue(this.currentValue)
targetPosition.updateValue(this.currentValue)

Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export interface MyLinkTargetConfig {
export interface MyLinkConfig {
ipAddress: string
systemID: string
port?: number
targets: MyLinkTargetConfig[]
compositeTargets: { [target: string]: string[] }
debug?: boolean
}

function normalizeTarget(
Expand Down
142 changes: 142 additions & 0 deletions src/my-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { createConnection, Socket } from 'net'
import { logDebug, logError } from './util'

class ConnectionManager {
private socket?: Socket
private previousRequestId = 0
private previousRequest: Promise<unknown> = Promise.resolve()

constructor(private host: string, private port: number) {}

private socketPromise?: Promise<Socket>

private openSocket() {
if (this.socketPromise) {
return this.socketPromise
}

this.socketPromise = new Promise<Socket>((resolve, reject) => {
const socket: Socket = createConnection(
{
host: this.host,
port: this.port,
},
() => {
resolve(socket)
}
)
socket.on('error', (e) => {
logDebug('Socket Error')
logDebug(e)
reject(e)
})
socket.on('close', () => {
logDebug('Socket Closed')
this.socket = undefined
})
this.socket = socket
})

return this.socketPromise
}

send<T = any>(payload: Record<string, unknown>) {
const request = this.previousRequest
.catch(() => null)
.then(async () => {
const socket = await this.openSocket(),
requestId = this.previousRequestId++

return new Promise<T>((resolve, reject) => {
function responseHandler(data: Buffer) {
try {
const response = JSON.parse(data.toString())
if (response.id === requestId) {
resolve(response)
}
} catch (e) {
logError(e)
reject(e)
} finally {
// eslint-disable-next-line no-use-before-define
cleanUp()
}
}
function cleanUp() {
socket.off('data', responseHandler)
}

socket.on('data', responseHandler)

setTimeout(() => {
cleanUp()
reject(new Error('Failed to send command after 1 second'))
}, 1000)

socket.write(
JSON.stringify({
id: requestId,
...payload,
}),
(error) => {
if (error) {
reject(error)
}
}
)
})
})

this.previousRequest = request
return request
}
}

interface JsonRpcResponse<T = any> {
jsonrpc: '2.0'
result: T
}

export interface ChannelInfo {
targetID: string
name: string
type: number
}

export class MyLink {
private connectionManager = new ConnectionManager(this.host, this.port)

constructor(
private systemID: string,
private host: string,
private port = 44100
) {}

getChannels() {
return this.send<ChannelInfo[]>('*.*', 'mylink.status.info')
}

async send<T = any>(targetID: string | undefined, method: string) {
const request = {
method,
params: {
auth: this.systemID,
targetID,
},
},
{ result } = await this.connectionManager.send<JsonRpcResponse<T>>(
request
)

return result
}

target(targetID: string) {
return {
down: () => this.send<boolean>(targetID, 'mylink.move.down'),
stop: () => this.send<boolean>(targetID, 'mylink.move.stop'),
up: () => this.send<boolean>(targetID, 'mylink.move.up'),
info: () => this.send<boolean>(targetID, 'mylink.status.info'),
}
}
}
33 changes: 23 additions & 10 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,43 @@ import { API, Logging, PlatformConfig, StaticPlatformPlugin } from 'homebridge'
import { MyLinkConfig, normalizeConfiguration } from './config'
import { AccessoryPlugin } from 'homebridge/lib/api'
import { SomfyMyLinkTargetAccessory } from './accessory'

const SomfySynergy = require('somfy-synergy')
import { useLogger } from './util'
import { MyLink } from './my-link'

export class SomfyMyLinkPlatform implements StaticPlatformPlugin {
private options = normalizeConfiguration(this.config, this.log)
private client = new SomfySynergy(
private myLink = new MyLink(
this.options.systemID,
this.options.ipAddress
)
public synergy = new SomfySynergy.Platform(
this.client,
this.options.compositeTargets
this.options.ipAddress,
this.options.port
)

constructor(
public log: Logging,
public config: PlatformConfig & MyLinkConfig,
public api: API
) {}
) {
useLogger({
logInfo(message) {
log.info(message)
},
logError(message) {
log.error(message)
},
logDebug(message) {
if (config.debug) {
log.info(message)
} else {
log.debug(message)
}
},
})
}

accessories(callback: (foundAccessories: AccessoryPlugin[]) => void) {
callback(
this.options.targets.map(
(target) => new SomfyMyLinkTargetAccessory(this, target)
(target) => new SomfyMyLinkTargetAccessory(this.myLink, target)
)
)
}
Expand Down
42 changes: 42 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
interface Logger {
logInfo: (message: string) => void
logError: (message: string) => void
logDebug: (message: string) => void
}

let logger: Logger = {
logInfo(message) {
// eslint-disable-next-line no-console
console.log(message)
},
logError(message) {
// eslint-disable-next-line no-console
console.error(message)
},
logDebug(message) {
// eslint-disable-next-line no-console
console.log(message)
},
}

export function delay(milliseconds: number) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds)
})
}

export function logDebug(message: any) {
logger.logDebug(message)
}

export function logInfo(message: any) {
logger.logInfo(message)
}

export function logError(message: any) {
logger.logError(message)
}

export function useLogger(newLogger: Logger) {
logger = newLogger
}

0 comments on commit 2f0e1f0

Please sign in to comment.