extensions/zwave-js-ws/ZWaveJs.lua
Rama master · réplica de lectura del repositorio original.
local logger = require('jls.lang.logger'):get(...)
local event = require('jls.lang.event')
local Promise = require('jls.lang.Promise')
local Exception = require('jls.lang.Exception')
local File = require('jls.io.File')
local json = require('jls.util.json')
local Map = require('jls.util.Map')
local WebSocket = require('jls.net.http.WebSocket')
local Thing = require('lha.Thing')
local utils = require('lha.utils')
return require('jls.lang.class').create(function(zWaveJs)
function zWaveJs:initialize(url, mapping)
self.url = url
self.mapping = utils.replaceRefs(mapping or {}, {
Thing = Thing, -- TODO remove
color = utils, -- TODO remove
utils = utils,
math = math,
})
-- TODO log duplicate devices mapping
end
function zWaveJs:updateConnectedState(value)
logger:fine('updateConnectedState(%s)', value)
end
function zWaveJs:publishEvent(e)
logger:finest('publishEvent()')
if self.onZWaveEvent then
self.onZWaveEvent(e)
end
end
function zWaveJs:setEventHandler(onZWaveEvent)
self.onZWaveEvent = onZWaveEvent
end
function zWaveJs:sendWebSocket(message, options)
if not self.webSocket then
return Promise.reject('not connected')
end
self.zwMsgId = self.zwMsgId + 1
local messageId = 'lha-zwave-js-'..tostring(self.zwMsgId)
if type(message) == 'string' then
message = {
command = message
}
elseif type(message) ~= 'table' then
error('Invalid message type '..type(message))
end
message.messageId = messageId
logger:finest('sendWebSocket(%s) %s', message.command, messageId)
if options then
Map.assign(message, options)
end
local textMsg = json.encode(message)
logger:finer('message: %s', textMsg)
return self.webSocket:sendTextMessage(textMsg):next(function()
local promise, cb = Promise.withCallback()
self.zwMsgMap[messageId] = {cb = cb, time = utils.time()}
return promise
end)
end
function zWaveJs:startListeningWebSocket()
-- start receive the state and get events
return self:sendWebSocket('start_listening'):next(function(result)
logger:info('Z-Wave start_listening found %d nodes', #result.state.nodes)
logger:finest('Z-Wave start_listening result: %T', result)
if self.dumpNodes then
logger:info('Z-Wave dumping nodes')
File:new('zwave-js.json'):write(json.stringify(result.state, 2))
end
return result.state
end)
end
function zWaveJs:startWebSocket()
self:close()
local webSocket = WebSocket:new(self.url)
local promise, cb = Promise.withCallback()
local timer = event:setTimeout(function()
cb('timeout')
end, 15000)
webSocket.onError = function(_, reason)
logger:warn('Z-Wave JS WebSocket error "%s"', reason)
end
webSocket.onClose = function()
self:updateConnectedState(false)
logger:warn('Z-Wave JS WebSocket closed')
end
webSocket.onTextMessage = function(_, payload)
logger:finest('Z-Wave WebSocket received %s', payload)
local status, message = Exception.pcall(json.decode, payload)
if status and message and message.type then
if message.type == 'event' and message.event then
logger:finer('Z-Wave JS event: %T', message.event)
self:publishEvent(message.event)
elseif message.type == 'result' then
local zwMsg = self.zwMsgMap[message.messageId]
if zwMsg then
logger:finer('Z-Wave JS WebSocket result %s', message.messageId)
self.zwMsgMap[message.messageId] = nil
if message.success and message.result then
zwMsg.cb(nil, message.result)
else
local reason = tostring(message.errorCode)
if message.errorCode == 'zwave_error' then
reason = tostring(message.zwaveErrorCode)..': '..tostring(message.zwaveErrorMessage)
end
zwMsg.cb(reason)
end
end
elseif message.type == 'version' then
-- command = 'driver.set_preferred_scale', scales = {temperature = 'Celsius'}
event:clearTimeout(timer)
cb()
else
logger:warn('Z-Wave JS WebSocket unsupported message type %s', message.type)
end
else
logger:warn('Z-Wave WebSocket received invalid JSON payload %s', payload)
end
end
self.webSocket = webSocket
webSocket:open():next(function()
webSocket:readStart()
logger:info('Z-Wave JS WebSocket connected on %s', self.url)
self:updateConnectedState(true)
--sendWebSocket('set_api_schema', {schemaVersion = 15})
end, function(reason)
self:updateConnectedState(false)
logger:warn('Cannot open Z-Wave JS WebSocket on %s due to %s', self.url, reason)
end)
return promise:catch(function(reason)
logger:warn('Z-Wave JS WebSocket start_listening failed "%s"', reason)
webSocket:close(false)
self.webSocket = nil
return Promise.reject(reason)
end)
end
function zWaveJs:close()
if self.webSocket then
self.webSocket:close()
self.webSocket = nil
end
if self.zwMsgMap then
for _, zwMsg in pairs(self.zwMsgMap) do
zwMsg.cb('timeout')
end
end
self.zwMsgId = 0
self.zwMsgMap = {}
end
function zWaveJs:refresh()
if self.webSocket and not self.webSocket:isClosed() then
local checkTime = utils.time()
local minMsgTime = checkTime - 30
for id, zwMsg in pairs(self.zwMsgMap) do
if zwMsg.time < minMsgTime then
self.zwMsgMap[id] = nil
zwMsg.cb('timeout')
end
end
else
self:startWebSocket()
end
end
function zWaveJs:findDeviceFromNode(node)
for _, device in ipairs(self.mapping.devices) do
if node.manufacturerId == device.manufacturerId and node.productId == device.productId and (node.productType == device.productType or not device.productType) then
return device
end
end
end
--[[
To uniquely identify a node value:
commandClass - The numeric identifier of the command class.
endpoint - (optional) The index of the node's endpoint (sub-device).
property - The name (or a numeric identifier) of the property, for example targetValue
propertyKey - (optional) Allows sub-addressing properties that contain multiple values (like combined sensors).
]]
-- https://github.com/OpenZWave/open-zwave/blob/master/config/manufacturer_specific.xml
function zWaveJs:createThingFromNode(node, device)
local title = utils.expand(self.mapping.title, node)
local description = utils.expand(self.mapping.description, node)
local thing = Thing:new(title, description)
if device.capabilities then
for _, capability in ipairs(device.capabilities) do
thing:addType(capability)
end
end
for _, info in ipairs(device.properties) do
utils.addThingPropertyFromInfo(thing, info.name, info, node)
end
if next(thing:getProperties()) then
return thing
end
end
-- an event only references the node id
function zWaveJs:updateThing(thing, device, nodeValue, isEvent)
for _, info in ipairs(device.properties) do
if nodeValue.commandClass == info.commandClass and nodeValue.property == info.property
and (not info.propertyKey or nodeValue.propertyKey == info.propertyKey)
and (not info.endpoint or nodeValue.endpoint == info.endpoint)
then
local value = isEvent and nodeValue.newValue or nodeValue.value
local isValue = utils.isValue(value)
if isValue then
if info.adapter then
value = info.adapter(value)
isValue = utils.isValue(value)
end
if isValue then
thing:updatePropertyValue(info.name, value)
end
end
break
end
end
end
function zWaveJs:setNodeValue(nodeId, device, name, value)
for _, info in ipairs(device.properties) do
if name == info.name then
return self:sendWebSocket({
command = 'node.set_value',
nodeId = nodeId,
valueId = {
commandClass = info.commandClass,
property = info.setProperty or 'targetValue'
},
value = value
})
end
end
return Promise.reject(string.format('No property for name "%s"', name))
end
end)