Ctrl K
ComunidadGuías…

extensions/zwave-js-ws/ZWaveJs.lua

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)