Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix connection issues #7

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Add your projector in your Hombridge `config.js`:
* `name` - the accessories name
* `address` - IP address of the projector
* `port` (optional) - Telnet port of the projector (default value: `23`)
* `pollInterval` (optional) - Projector status polling interval in seconds (default value: `1`, disable: `0`)
* `projectorId` (optional) - The ID of the projector (default value: `1`)
* `model` (optional) - The projectors model (default value: `unkown`)
* `serialNumber` (optional) - The projectors serial number (default value: `unknown`)
* `serialNumber` (optional) - The projectors serial number (default value: `unknown`)
* `logLevel` (optional) - 0: off, 1: default, 3: verbose (default value: `1`)
18 changes: 18 additions & 0 deletions decode.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
OK0007}700C00400
OK10023400C00400



OK10023400C00400

OK10023408C00401
OK10023408C00403
OK10023408C00403

OK 1 00234 08 C004 03

onoff
hours
source
firmware
display mode
208 changes: 152 additions & 56 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ let Service, Characteristic

const net = require('net')

const ON = 1
const OFF = 0
const PORT = 23
const MAX_RETRIES = 3
const SECOND = 1000
const POLL_INTERVAL = 1000
const MAX_POLL_RETRIES = 10
const PAUSE_UPDATE_TIME = 9000
const CONNECTION_TIMEOUT = 30000
const CMD_TIMEOUT = 1500
const DEFAULT_LOG_LEVEL = 1
const DEBUG = 2

const UP_MESSAGES = [
'INFO1',
Expand All @@ -17,6 +29,8 @@ const DOWN_MESSAGES = [
'Ok0'
]

const formatData = (data) => String(data).split('\r\n').slice(0, -1).join()

module.exports = (homebridge) => {
Service = homebridge.hap.Service
Characteristic = homebridge.hap.Characteristic
Expand All @@ -26,13 +40,37 @@ module.exports = (homebridge) => {
class ProjectorAccessory {

constructor(log, config) {
this.connectionTimeout = 30000
this.log = log
this.preflight(log, config)

this.config = config
this.error = true
this.logLevel = config.logLevel || DEFAULT_LOG_LEVEL
this.pollInterval = config.pollInterval * SECOND || POLL_INTERVAL
this.maxRetries = config.maxRetries || MAX_RETRIES
this.retryCount = 0

this.log = (msg, level = DEFAULT_LOG_LEVEL) => {
if (level<=this.logLevel) log(msg)
}

this.enableStatusUpdates = true
this.connected = false
this.pollRequestCount = 0
this.data = null
this.isOn = null
this.poll = null

this.log(`Log level: ${this.logLevel}`)
this.connect()
this.service = new Service.Switch(this.config.name)
this.updateStatus(0)
}

preflight(log, config) {
if (config.name === undefined) {
log.error("ERROR: name not found in config")
}
if (config.address === undefined) {
log.error("ERROR: address not found in config")
}
}

getServices() {
Expand All @@ -54,107 +92,165 @@ class ProjectorAccessory {
this.socket.destroy()
}

if (this.poll) {
clearInterval(this.poll)
}

const socketOptions = {
host: this.config.address,
port: parseInt(this.config.port) || 23
port: Number(this.config.port) || PORT,
noDelay: true,
keepAlive: true,
}

this.socket = new net.Socket();
this.socket = new net.Socket()
this.socket.on('error', this.handleError.bind(this))
this.socket.on('timeout', this.handleError.bind(this))
this.socket.on('timeout', this.handleTimeout.bind(this))
this.socket.on('data', this.handleData.bind(this))
this.socket.setTimeout(this.connectionTimeout)

this.socket.on('ready', this.handleReady.bind(this))
this.socket.setTimeout(CONNECTION_TIMEOUT)

this.log('Trying to connect...')
this.socket.connect(socketOptions, () => {
this.socket.setTimeout(0)
this.log('Projector is connected.')
this.error = false
this.connected = true
this.socket.write(this.getStatusCommand())
})
}

pollStatus() {
if (!this.enableStatusUpdates) return

if (this.pollRequestCount === MAX_POLL_RETRIES) {
this.log(`No Respose (polling)`, DEBUG)
this.resetConnection()
return
}

const getStatusCommand = this.getStatusCommand()
this.log(`Request status (${formatData(getStatusCommand)}) ${this.pollRequestCount}`, DEBUG)
this.socket.write(getStatusCommand)
this.pollRequestCount++
}

updateStatus(status) {
this.isOn = status;
this.service.getCharacteristic(Characteristic.On).updateValue(status);
if (!this.enableStatusUpdates) return
this.service.getCharacteristic(Characteristic.On).updateValue(status)

if (this.isOn === status) return
this.isOn = status
this.log(Boolean(status) ? 'ON' : 'OFF')
}

getSocketOptions() {
return {
host: this.config.address,
port: parseInt(this.config.port) || 23
handleReady() {
this.log('Connected')
if (this.pollInterval !== 0) {
this.poll = setInterval(this.pollStatus.bind(this), this.pollInterval)
}
if (this.isOn !== null) {
this.sendStatusCmd[Number(this.isOn)].bind(this)()
}
}

handleError() {
this.log('Socket error')
this.error = true
this.updateStatus(0)
this.resetConnection()
}

handleTimeout() {
this.log('Socket timeout')
this.resetConnection()
}

resetConnection() {
this.connected = false
this.pollRequestCount = 0
this.connect()

}

handleCallback(data, value) {
this.commandCallback(value)
this.commandCallback = null
}

handleData(data) {
if (this.messageInData(data, UP_MESSAGES)) {
this.log('Received UP message.')
this.updateStatus(1)
this.log(`Received: ${formatData(data)}`, DEBUG)
if (this.data !== String(data) && this.logLevel !== DEBUG) {
this.log(`Received: ${formatData(data)}`)
this.data = String(data)
}

if (this.messageInData(data, DOWN_MESSAGES)) {
this.log('Received DOWN message.')
this.updateStatus(0)

if (this.messageInData(data, UP_MESSAGES)) {
this.updateStatus(ON)
} else if (this.messageInData(data, DOWN_MESSAGES)) {
this.updateStatus(OFF)
}

if (this.commandCallback) {
if (data.includes('P')) {
this.commandCallback(null)
}
if (data.includes('F')) {
this.commandCallback(true)
}
this.commandCallback = null
this.handleCallback(data, null)
} else if (data.includes('F')) {
this.handleCallback(data, true)
}
} else {
this.pollRequestCount = 0
}
}

messageInData(data, messages) {
for (const message of messages) {
if (data.includes(message)) {
return true
}
}
return false
messageInData = (data, messages) => messages.some(message => data.includes(message))

pauseUpdate() {
this.log('Disable status updates', DEBUG)
this.enableStatusUpdates = false
setTimeout(() => {
this.log('Enable status updates', DEBUG)
this.enableStatusUpdates = true
}, PAUSE_UPDATE_TIME)
}

sendStatusCmd = [
() => {
const shutdownCommand = this.getShutdownCommand()
this.log(`Sending shutdown command (${formatData(shutdownCommand)})`, DEBUG)
this.socket.write(shutdownCommand)
this.updateStatus(OFF)
},
() => {
const bootCommand = this.getBootCommand()
this.log(`Sending boot command (${formatData(bootCommand)})`, DEBUG)
this.socket.write(bootCommand)
this.updateStatus(ON)
},
]

setOnCharacteristicHandler(value, callback) {
if (this.error) {
if (!this.connected) {
callback(true)
return
}
let oldValue = value

if (value) {
this.socket.write(this.getBootCommand());
this.log('Sending boot command')
} else {
this.socket.write(this.getShutdownCommand())
this.log('Sending shutdown command')
}
this.sendStatusCmd[Number(value)].bind(this)()

this.pauseUpdate()

this.commandCallback = callback

setTimeout(() => {
if (this.commandCallback) {
this.handleError()
this.updateStatus(oldValue)
this.commandCallback(true)
this.commandCallback = null
if (this.retryCount++ >= this.maxRetries) {
this.log('Connection failure')
callback(true)
return
}
this.log(`No Respose (${this.retryCount})`)
this.resetConnection()
}
}, 5000)

this.updateStatus(value)
}, CMD_TIMEOUT)
}

getOnCharacteristicHandler(callback) {
if (this.error) {
if (!this.connected) {
callback(true)
return
}
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "homebridge-optoma-projector-telnet",
"displayName": "Homebridge Optoma Projector Telnet",
"version": "1.0.7",
"displayName": "Optoma Projector Telnet",
"version": "1.0.8",
"description": "A plugin for Homebridge, to control Optoma Projectors via Telnet.",
"main": "index.js",
"repository": {
Expand All @@ -17,7 +17,6 @@
"keywords": [
"homebridge-plugin",
"optoma",
"rs232",
"telnet",
"homebridge",
"homekit",
Expand All @@ -27,6 +26,9 @@
"node": ">=10.17.0",
"homebridge": ">=0.4.22"
},
"author": "Simon Rauch <[email protected]>",
"authors": [
"Simon Rauch <[email protected]>",
"David J. Bradshaw <[email protected]>"
],
"license": "MIT"
}