srveit/insteon-hub2

View on GitHub
lib/encodeCommand.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use strict'

// Also look at
//   2475SDBdev-112011-en.pdf
//   2477Sdev-072012-en.pdf
//   2663-222dev-062014-en.pdf
//   2845-222dev-102013-en.pdf
// 2477D, 2477DH, 2477S, 2663-222, 2845-222
const { INSTEON_MESSAGE_TYPES, OUTLET_CODES, ALL_LINK_CODES, OPERATING_FLAGS } =
    require('./constants')
const encoders = {}
const commandResponseMatchers = {}

const commandResponseMatcher = command =>
  (commandResponseMatchers[command.command] || (response => {
    /* eslint no-console: "off" */
    console.warn(`no matcher for command ${command.command}`, response)
    return false
  }))

const toHex1 = (value) => Math.min(15, Math.max(0, value || 0)).toString(16)
  .toUpperCase()

const toHex = (value) => Math.min(255, Math.max(0, value || 0)).toString(16)
  .padStart(2, '0').toUpperCase().substring(0, 2)

const toHexSigned = (value) => ((value || 0) < 0)
  ? toHex(Math.max(-128, value) + 256, 2)
  : toHex(Math.min(127, value || 0), 2)

const encodeHex = property => command => toHex(command[property])

const encodeMessageFlags = command => {
  const messageType = INSTEON_MESSAGE_TYPES.indexOf(command.messageType) << 5
  const extendedMessage = (command.extendedMessage ? 1 : 0) << 4
  const hopsLeft = (typeof command.hopsLeft === 'number' ? command.hopsLeft : 3) << 2
  const maxHops = typeof command.maxHops === 'number' ? command.maxHops : 3
  const byte = messageType + extendedMessage + hopsLeft + maxHops
  return toHex(byte)
}

const encodeOnLevel = (onLevel = 0) => {
  return toHex(onLevel || 0).substring(0, 1)
}

// ramp rate 0-1F 0.1 to 9 minutes
const encodeRampRate = (rampRate = 0) => {
  return toHex1(Math.floor(((rampRate || 0) - 1) / 2 + 0.5))
}

const encodeOutlet = command => OUTLET_CODES[command.outlet]

const encodeOperatingFlag = command => OPERATING_FLAGS[command.flag]

const encodeDirection = command => command.direction === 'up' ? '01' : '00'

const encodeOnLevelAndRampRate = command =>
  encodeOnLevel(command.onLevel) + encodeRampRate(command.rampRate)

const encodeX10Address = command =>
  toHex(command.groupNumber) +
    '04' +
    toHex(command.houseCode) +
    toHex(command.unitCode)

// encodeLEDBrightness = command =>
//   '00' +
//   '07' +
//   toHex(command.ledBrightness),

const encodeTriggerGroupInfo = command =>
  toHex(command.groupNumber) +
    (command.useLocalOnLevel ? '00' : '01') +
    toHex(command.onLevel) +
    '3000' +
    (command.useLocalRampRate ? '00' : '01')

const encodeDatabaseRange = command => {
  return '00' +
    '00' +
    (command.address ? command.address.substring(0, 4) : '0000') +
    (command.numberRecords ? toHex(command.numberRecords) : '00')
}

const encodeImConfigurationFlags = flags => {
  const disableAutomaticLinking =
        (flags.disableAutomaticLinking ? 1 : 0) << 7
  const monitorMode = (flags.monitorMode ? 1 : 0) << 6
  const disableAutomaticLed = (flags.disableAutomaticLed ? 1 : 0) << 5
  const disableHostComunications = (flags.disableHostComunications ? 1 : 0) << 4
  const reserved = (flags.reserved || 0) & 0xF

  const byte = disableAutomaticLinking + monitorMode + disableAutomaticLed +
        disableHostComunications + reserved
  return toHex(byte)
}

const crcByte = (messageBytes) => {
  const allBytes = messageBytes.match(/.{1,2}/g).reduce(
    (sum, byte) => sum + parseInt(byte, 16),
    0
  )
  return toHex(((~allBytes) + 1) & 0xFF)
}

const encodeEdCommandWithCrc = command => {
  const messageBytes = (command.command1 + (command.command2 || '00') +
                          (command.userData || '').padEnd(26, '0')).substring(0, 30)
  return messageBytes + crcByte(messageBytes)
}

const im = (name, code, data, matcher) => {
  const encoder = command =>
    '02' + code + (typeof data === 'function' ? data(command) : '')
  encoder.type = 'IM'
  encoder.commandName = name
  encoders[name] = encoder
  commandResponseMatchers[name] = matcher ||
      (response => response.command === name)
}

const sd = (name, command1, command2 = '00') => {
  const encoder = command =>
  /* eslint no-nested-ternary: "off" */
    encoders['Send INSTEON Standard-length Message']({
      messageType: 'direct',
      toAddress: command.toAddress,
      command1,
      command2: typeof command2 === 'function'
        ? command2(command)
        : (command2 === '00' && command.command2 ? command.command2 : command2),
      hopsLeft: command.hopsLeft,
      maxHops: command.maxHops,
    })
  encoder.type = 'SD'
  encoder.commandName = name
  encoders[name] = encoder
  commandResponseMatchers[name] = response => (
    response.command === 'INSTEON Standard Message Received' &&
        response.insteonCommand &&
        response.insteonCommand.command === name
  )
}

const ed = (name, command1, command2 = '00', userData = '', matcher) => {
  const encoder = command => {
    return encoders['Send INSTEON Extended-length Message']({
      messageType: 'direct',
      toAddress: command.toAddress,
      command1,
      command2: typeof command2 === 'function' ? command2(command) : command2,
      userData: typeof userData === 'function' ? userData(command) : userData,
      hopsLeft: command.hopsLeft,
      maxHops: command.maxHops,
    })
  }
  encoder.type = 'ED'
  encoder.commandName = name
  encoders[name] = encoder
  commandResponseMatchers[name] = matcher || (response => (
    response.command === 'INSTEON Standard Message Received' &&
        response.command1 === command1 &&
        response.command2 === command2
  ))
}

const sa = (name, command1, command2 = '00') => {
  const encoder = command =>
    encoders['Send INSTEON Standard-length Message']({
      messageType: 'allLinkBroadcast',
      toAddress: '0000' + toHex(command.groupNumber),
      command1,
      command2,
      hopsLeft: command.hopsLeft,
      maxHops: command.maxHops,
    })
  encoder.type = 'SA'
  encoder.commandName = name
  encoders[name] = encoder
}

const sb = (name, command1, command2 = '00') => {
  const encoder = command =>
    encoders['Send INSTEON Standard-length Message']({
      messageType: 'broadcast',
      toAddress: toHex(command.deviceCategory) + toHex(command.deviceSubcategory) + 'FF',
      command1,
      command2,
      hopsLeft: command.hopsLeft,
      maxHops: command.maxHops,
    })
  encoder.type = 'SD'
  encoder.commandName = name
  encoders[name] = encoder
}

const encodeCommand = command => {
  const encoder = encoders[command.command]
  if (encoder) {
    return encoder(command)
  }
  return ''
}

// Modem commands
im('Get IM Info', '60')

im('Send ALL-Link Command', '61', command =>
  toHex(command.groupNumber) +
  command.allLinkCommand +
  (command.command2 || '00'))

im('Send INSTEON Standard-length Message', '62', command =>
  (command.toAddress || '').padStart(6, '0').substring(0, 6) +
  encodeMessageFlags(command) +
  command.command1 +
  (command.command2 || '00'))

im('Send INSTEON Extended-length Message', '62', command =>
  (command.toAddress || '').padStart(6, '0').substring(0, 6) +
  encodeMessageFlags(Object.assign({ extendedMessage: true }, command)) +
  encodeEdCommandWithCrc(command))
// 63 TODO: Send X10

im('Start ALL-Linking', '64', command =>
  (ALL_LINK_CODES[command.allLinkType] || '00') + toHex(command.groupNumber))

im('Cancel ALL-Linking', '65')

im('Set Host Device Category', '66', command =>
  (command.deviceCategory || '00') +
  (command.deviceSubcategory || '00') +
  (command.firmware || '00'))
// TODO: adjust timeout since this can take 20 secs to return

im('Reset the IM', '67')

im('Set INSTEON ACK Message Byte', '68', command =>
  (command.command2Data || '00'))

im('Get First ALL-Link Record', '69', '', response =>
  (response.command === 'Get First ALL-Link Record' && !response.ack) ||
  response.command === 'ALL-Link Record Response')

im('Get Next ALL-Link Record', '6A', '', response =>
  (response.command === 'Get Next ALL-Link Record' && !response.ack) ||
  response.command === 'ALL-Link Record Response')

im('Set IM Configuration', '6B', command =>
  encodeImConfigurationFlags(command.imConfigurationFlags || {}))

im('Get ALL-Link Record for Sender', '6C', '', response =>
  (response.command === 'Get ALL-Link Record for Sender' && !response.ack) ||
  response.command === 'ALL-Link Record Response')

im('IM LED On', '6D')

im('IM LED Off', '6E')
// 6F TODO: Manage ALL-Link Record

im('Set INSTEON NAK Message Byte', '70', command =>
  (command.command2Data || '00'))

im('Set INSTEON ACK Message Two Bytes', '71', command =>
  (command.command1Data || '00') +
  (command.command2Data || '00'))

im('RF Sleep', '72')

im('Get IM Configuration', '73')

im('Cancel Cleanup', '74', () => '00')

im('Read 8 bytes from Database', '75', command =>
  (command.address || '').padStart(4, '0').substring(0, 4), response =>
  (response.command === 'Read 8 bytes from Database' && !response.ack) ||
    response.command === 'Database Record Found')

// 76 TODO: Write 8 bytes to Database

// Note: the documentation does not mention the 00 byte
im('Beep IM', '77', () => '00')

im('Set Status', '78', command => (command.status || '00'))

im('Set Database Link Data for next Link', '79', command =>
  (command.data || '').padEnd(6, '0'))

im('Set Application Retries for New Links', '7A', command =>
  toHex(command.retries || 0))

im('Set RF Frequency Offset', '7B', command =>
  toHexSigned(command.refFrequencyOffset))

im('Set Acknowledge for TempLinc command', '7C', command =>
  (command.acknowledge || '').padEnd(16, '0').substring(0, 16))

// 7D TODO: unknown

// 7E TODO: unknown

im('7F Command', '7F', command =>
  (command.data ? command.data.padStart(2, '0').substring(0, 2) : '00'))

// Standard-length Direct Commands
// 00 Reserved

sd('Assign to ALL-Link Group', '01', encodeHex('groupNumber'))

sd('Delete from ALL-Link Group', '02', encodeHex('groupNumber'))

sd('Product Data Request', '03', '00')

sd('FX Username Request', '03', '01')

sd('Device Text String Request', '03', '02')

// 04-08 Reserved

sd('Enter Linking Mode', '09', encodeHex('groupNumber'))

sd('Enter Unlinking Mode', '0A', encodeHex('groupNumber'))

// 0B-0C Reserved

sd('Get INSTEON Engine Version', '0D', '00')

// 0E Reserved

sd('Ping', '0F')

sd('ID Request', '10')

sd('Light ON', '11', encodeHex('onLevel'))

sd('Light ON Fast', '12', encodeHex('onLevel'))

sd('Light OFF', '13')

sd('Light OFF Fast', '14')

sd('Light Brighten One Step', '15')

sd('Light Dim One Step', '16')

sd('Light Start Manual Change', '17', encodeDirection)

sd('Light Stop Manual Change', '18')

sd('Light Status On-Level Request', '19', '00')

sd('Light Status LED Request', '19', '01')

sd('Outlet Status Request', '19', '01')

sd('Light Status Request 02', '19', '02')

// 1A-1E Reserved

sd('Get Operating Flags', '1F', '00')

sd('Get ALL-Link Database Delta', '1F', '01')

sd('Get Signal-to-Noise Value', '1F', '02')

sd('Get Operating Flags 2', '1F', '05')

sd('Set Operating Flags', '20', encodeOperatingFlag)

sd('Light Instant Change', '21', encodeHex('onLevel'))

sd('Light Manually Turned Off', '22')

sd('Light Manually Turned On', '23')

// Deprecated
sd('Reread Init Values', '24')

sd('Remote SET Button Tap', '25', '01')

sd('Remote SET Button Tap Twice', '25', '02')

// 26 Reserved

sd('Light Set Status', '27', encodeHex('onLevel'))

// 28-2D Deprecated

sd('Light ON at Ramp Rate', '2E', encodeOnLevelAndRampRate)

// 2F Reserved

sd('Beep Device', '30')

// 31 Reserved

sd('Outlet ON', '32', encodeOutlet)

sd('Outlet OFF', '33', encodeOutlet)

// 34-3F Reserved

// Extended-length Direct Commands
ed('Remote Enter Linking Mode', '09', encodeHex('groupNumber'))

ed('Remote Enter Unlinking Mode', '0A', encodeHex('groupNumber'))

ed('ON (Bottom Outlet)', '11', encodeHex('onLevel'), '02')

ed('OFF (Bottom Outlet)', '13', '00', '02')

ed('Programming Lock On', '20', '00')

ed('Programming Lock Off', '20', '01')

ed('LED Blink on Traffic On', '20', '02')

ed('LED Blink on Traffic Off', '20', '03')

ed('Beeper On', '20', '04')

ed('Resume Dim On', '20', '04')

ed('Load Sense On (Bottom Outlet)', '20', '04')

ed('Resume Dim Off', '20', '05')

ed('Beeper Off', '20', '05')

ed('Load Sense Off (Bottom Outlet)', '20', '05')

ed('Stay Awake On', '20', '06')

ed('8-Key KeypadLinc', '20', '06')

ed('Load Sense On (Top Outlet)', '20', '06')

ed('Stay Awake Off', '20', '07')

ed('6-Key KeypadLinc', '20', '07')

ed('Load Sense Off (Top Outlet)', '20', '07')

ed('Listen Only Off', '20', '08')

ed('LED Off', '20', '08')

ed('Listen Only On', '20', '09')

ed('LED On', '20', '09')

ed('No I\'m Alive On', '20', '0A')

ed('Keybeep On', '20', '0A')

ed('No I\'m Alive Off', '20', '0B')

ed('Keybeep Off', '20', '0B')

ed('RF Off', '20', '0C')

ed('RF On', '20', '0D')

ed('Powerline Off', '20', '0E')

ed('Powerline On', '20', '0F')

ed('X10 Off', '20', '12')

ed('X10 On', '20', '13')

ed('Error Blink Off', '20', '14')

ed('Error Blink On', '20', '15')

ed('Cleanup Report Off', '20', '16')

ed('Cleanup Report On', '20', '17')

ed('Smart Hops On', '20', '1C')

ed('Smart Hops Off', '20', '1D')

ed('Get for Group/Button', '2E', '00', encodeHex('groupNumber'))

ed('Set X10 Address', '2E', '00', encodeX10Address)

ed('Set Ramp Rate', '2E', '00', command => '0005' + encodeRampRate(command.rampRate))

ed('Set On Level', '2E', '00', command => '0006' + toHex(command.onLevel))

ed('Set LED Brightness', '2E', '00', command => '0007' + toHex(command.ledBrightness))

ed('Read ALL-Link Database', '2F', '00', encodeDatabaseRange)

// TODO: implement
// ed('Write ALL-Link Database', '2F', '00', encodeWriteDatabaseRange);

ed('Trigger Group', '30', '00', encodeTriggerGroupInfo)

// All-LINK Broadcast Commands
sa('ALL-Link Alias 1 Low', '13')

// Standard-length Broadcast Commands
sb('Test Powerline Phase A', '03', '00')

sb('Test Powerline Phase B', '03', '01')

exports.encodeCommand = encodeCommand
exports.commandResponseMatcher = commandResponseMatcher