node-red-contrib-homekit-bridged
Version:
Node-RED nodes to simulate Apple HomeKit devices.
473 lines (418 loc) • 21.2 kB
HTML
<style>
.alert {
position: relative;
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: .25rem;
margin-right: 60px;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
</style>
<script type="text/javascript">
const initExperimental = function () {
//NRCHKB Custom Characteristics
$.ajax({
url: 'nrchkb/config',
dataType: 'json',
async: false,
success: function (data) {
nrchkbConfig = data
},
})
RED.nodes.registerType('nrchkb', {
category: 'Apple HomeKit',
icon: 'nrchkb.png',
inputs: 0,
outputs: 0,
defaults: {
customCharacteristics: {
value: []
}
},
label: function () {
return 'NRCHKB'
},
labelStyle: function () {
return 'node_label_italic'
},
oneditsave: function () {
const self = this
saveCustomCharacteristics(self)
},
oneditprepare: function () {
/*$("#node-input-customCharacteristics")
.val(JSON.stringify(config.customCharacteristics) || [])
.typedInput({
type: "json",
types: ["json"]
})*/
$('#node-input-customCharacteristics-container').css('min-height', '150px').css('min-width', '550px').editableList({
addItem: function (container, i, opt) {
$('.properties-accordion').accordion('option', 'active', false)
const {name, UUID, ...props} = opt
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
})
const fragment = document.createDocumentFragment()
const row1 = $('<div/>', {style: 'display:flex; align-items: center;'}).appendTo(fragment)
const row2 = $('<div/>', {style: 'display:flex; margin-top:8px; align-items: center;'}).appendTo(fragment)
const row3 = $('<div/>', {style: 'margin-top:8px; align-items: center;'}).appendTo(fragment)
$('<div/>', {
style: 'display:inline-block;text-align:left; width:120px; padding-left:10px; box-sizing:border-box;',
required: 'required'
})
.text('UUID ')
.appendTo(row1)
$('<input/>', {class: 'property-uuid', type: 'text'})
.val(UUID ? UUID : uuidv4)
.appendTo(row1)
$('<div/>', {
style: 'display:inline-block;text-align:left; width:120px; padding-left:10px; box-sizing:border-box;',
required: 'required'
})
.text('Name ')
.appendTo(row2)
$('<input/>', {class: 'property-name', type: 'text'})
.val(name)
.appendTo(row2)
const row3_properties_accordion = $('<div/>', {class: 'properties-accordion'})
.appendTo(row3)
$('<h3/>').text('Properties').appendTo(row3_properties_accordion)
const row3_properties = $('<div/>', {class: 'properties'}).appendTo(row3_properties_accordion)
const formatRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-format'
}).text('Format *').appendTo(formatRow)
const formatInput = $('<select/>', {
class: 'property-format',
required: 'required'
}).appendTo(formatRow)
$('<option/>').val(undefined).text('Choose...').appendTo(formatInput)
$('<option/>').val('bool').text('BOOL').appendTo(formatInput)
$('<option/>').val('int').text('INT').appendTo(formatInput)
$('<option/>').val('float').text('FLOAT').appendTo(formatInput)
$('<option/>').val('string').text('STRING').appendTo(formatInput)
$('<option/>').val('uint8').text('UINT8').appendTo(formatInput)
$('<option/>').val('uint16').text('UINT16').appendTo(formatInput)
$('<option/>').val('uint32').text('UINT32').appendTo(formatInput)
$('<option/>').val('uint64').text('UINT64').appendTo(formatInput)
$('<option/>').val('data').text('DATA').appendTo(formatInput)
$('<option/>').val('tlv8').text('TLV8').appendTo(formatInput)
$('<option/>').val('array').text('ARRAY').appendTo(formatInput)
$('<option/>').val('dict').text('DICTIONARY').appendTo(formatInput)
formatInput.val(props.format)
const unitRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {class: 'form-row', htmlFor: 'property-unit'}).text('Unit').appendTo(unitRow)
const unitSelect = $('<select/>', {class: 'property-unit'}).appendTo(unitRow)
$('<option/>').val(undefined).text('Choose...').appendTo(unitSelect)
$('<option/>').val('celsius').text('CELSIUS').appendTo(unitSelect)
$('<option/>').val('percentage').text('PERCENTAGE').appendTo(unitSelect)
$('<option/>').val('arcdegrees').text('ARC_DEGREE').appendTo(unitSelect)
$('<option/>').val('lux').text('LUX').appendTo(unitSelect)
$('<option/>').val('seconds').text('SECONDS').appendTo(unitSelect)
unitSelect.val(props.unit)
const permsRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-perms'
}).text('Permissions').appendTo(permsRow)
const permsSelect = $('<select/>', {
class: 'property-perms',
multiple: 'multiple'
}).appendTo(permsRow)
$('<option/>').val('pr').text('PAIRED_READ / READ').appendTo(permsSelect)
$('<option/>').val('pw').text('PAIRED_WRITE / WRITE').appendTo(permsSelect)
$('<option/>').val('ev').text('NOTIFY / EVENTS').appendTo(permsSelect)
$('<option/>').val('aa').text('ADDITIONAL_AUTHORIZATION').appendTo(permsSelect)
$('<option/>').val('tw').text('TIMED_WRITE').appendTo(permsSelect)
$('<option/>').val('hd').text('HIDDEN').appendTo(permsSelect)
$('<option/>').val('wr').text('WRITE_RESPONSE').appendTo(permsSelect)
permsSelect.val(props.perms)
const evRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-ev'
}).text('Event Notifications').appendTo(evRow)
$('<input/>', {
class: 'property-ev',
type: 'checkbox',
checked: props.ev
}).appendTo(evRow)
const descriptionRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-description'
}).text('Description').appendTo(descriptionRow)
$('<input/>', {
class: 'property-description',
type: 'text'
}).appendTo(descriptionRow).val(props.description)
const minValueRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-minValue'
}).text('Minimum Value').appendTo(minValueRow)
$('<input/>', {
class: 'property-minValue',
type: 'number'
}).appendTo(minValueRow).val(props.minValue)
const maxValueRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-maxValue'
}).text('Maximum Value').appendTo(maxValueRow)
$('<input/>', {
class: 'property-maxValue',
type: 'number'
}).appendTo(maxValueRow).val(props.maxValue)
const minStepRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-minStep'
}).text('Minimum Step').appendTo(minStepRow)
$('<input/>', {
class: 'property-minStep',
type: 'number'
}).appendTo(minStepRow).val(props.minStep)
const maxLenRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-maxLen'
}).text('Maximum Length').appendTo(maxLenRow)
$('<input/>', {
class: 'property-maxLen',
type: 'number'
}).appendTo(maxLenRow).val(props.maxLen)
const maxDataLenRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-maxDataLen'
}).text('Maximum Data Length').appendTo(maxDataLenRow)
$('<input/>', {
class: 'property-maxDataLen',
type: 'number'
}).appendTo(maxDataLenRow).val(props.maxDataLen)
const validValuesRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-validValues'
}).text('Valid Values').appendTo(validValuesRow)
$('<input/>', {
class: 'property-validValues',
type: 'text'
}).appendTo(validValuesRow).val(props.validValues)
const validValueRangesRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
const validValueRangesLabel = $('<label/>', {
class: 'form-row property-validValueRanges-label',
htmlFor: 'property-validValueRanges'
}).text('Valid Value Ranges: ').appendTo(validValueRangesRow)
const validValueRangesSlider = $('<div/>', {
class: 'property-validValueRanges',
type: 'text'
}).appendTo(validValueRangesRow)
validValueRangesSlider.slider({
range: true,
min: 0,
max: 500,
values: props.validValueRanges,
slide: function (event, ui) {
validValueRangesLabel.text('Valid Value Ranges: [' + ui.values[0] + ', ' + ui.values[1] + ']')
}
})
validValueRangesLabel.text('Valid Value Ranges: [' + validValueRangesSlider.slider('values', 0) + ', ' + validValueRangesSlider.slider('values', 1) + ']')
const adminOnlyAccessRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
$('<label/>', {
class: 'form-row',
htmlFor: 'property-adminOnlyAccess'
}).text('Admin Only Access').appendTo(adminOnlyAccessRow)
const adminOnlyAccessSelect = $('<select/>', {
class: 'property-adminOnlyAccess',
multiple: 'multiple'
}).appendTo(adminOnlyAccessRow)
$('<option/>').val(0).text('READ').appendTo(adminOnlyAccessSelect)
$('<option/>').val(1).text('WRITE').appendTo(adminOnlyAccessSelect)
$('<option/>').val(2).text('NOTIFY').appendTo(adminOnlyAccessSelect)
adminOnlyAccessSelect.val(props.adminOnlyAccess)
row3_properties_accordion.accordion({
collapsible: true,
active: 'UUID' in opt ? false : 0,
heightStyle: 'content'
})
container[0].appendChild(fragment)
},
removable: true,
sortable: true
})
for (let i = 0; i < nrchkbConfig.customCharacteristics.length; i++) {
const customCharacteristic = nrchkbConfig.customCharacteristics[i]
$('#node-input-customCharacteristics-container').editableList('addItem', customCharacteristic)
}
},
oneditresize: function (size) {
const rows = $('#dialog-form>div:not(.node-input-customCharacteristics-container-row)')
let height = size.height
for (let i = 0; i < rows.length; i++) {
height -= $(rows[i]).outerHeight(true)
}
const editorRow = $('#dialog-form>div.node-input-customCharacteristics-container-row')
height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom')))
height += 16
$('#node-input-customCharacteristics-container').editableList('height', height)
}
})
}
</script>
<script type="text/javascript">
RED.validators.port = function (port) {
return port !== 1880 && port >= 1 && port <= 65535 && port === port.toString()
}
const isValueDefined = function (value) {
return 'undefined' === typeof value ? false : null !== value
}
let serviceTypes
let accessoryCategories
let nrchkbVersion = '0.0.0'
let nrchkbExperimental = false
let nrchkbConfig = {}
//HomeKit Service Types
$.getJSON('nrchkb/service/types', function (data) {
serviceTypes = data
})
//HomeKit Accessory Categories
$.getJSON('nrchkb/accessory/categories', function (data) {
accessoryCategories = data
})
//NRCHKB Info
$.ajax({
url: 'nrchkb/info',
dataType: 'json',
async: false,
success: function (data) {
nrchkbVersion = data.version
nrchkbExperimental = data.experimental
if (nrchkbExperimental) {
initExperimental()
}
},
})
const versionValidator = function (value) {
return value ? /^(\d+\.)?(\d+\.)?(\.|\d+)$/.test(value) : true
}
const hostNameValidator = function (value) {
return value ? /^[^.]{1,64}$/.test(value) : false
}
const cameraConfigRequiredField = function (value) {
return 'CameraControl' === this.serviceName ? (value || '').toString().trim() : true
}
const saveCustomCharacteristics = function (self) {
const customCharacteristics = []
const items = $('#node-input-customCharacteristics-container').editableList('items')
items.each(function () {
const item = $(this)
const name = item.find('.property-name').val()
const UUID = item.find('.property-uuid').val()
const format = item.find('.property-format').val()
const unit = item.find('.property-unit').val()
const perms = item.find('.property-perms').val()
const ev = item.find('.property-ev').prop('checked')
const description = item.find('.property-description').val()
const minValue = item.find('.property-minValue').val()
const maxValue = item.find('.property-maxValue').val()
const minStep = item.find('.property-minStep').val()
const maxLen = item.find('.property-maxLen').val()
const maxDataLen = item.find('.property-maxDataLen').val()
const validValues = item.find('.property-validValues').val()
const validValueRangesMin = item.find('.property-validValueRanges').slider('values', 0)
const validValueRangesMax = item.find('.property-validValueRanges').slider('values', 1)
const validValueRanges = validValueRangesMin && validValueRangesMax ? [validValueRangesMin, validValueRangesMax] : undefined
const adminOnlyAccess = item.find('.property-adminOnlyAccess').val()
customCharacteristics.push({
name,
UUID,
format,
unit,
perms,
ev,
description,
minValue,
maxValue,
minStep,
maxLen,
maxDataLen,
validValues,
validValueRanges,
adminOnlyAccess
})
})
nrchkbConfig.customCharacteristics = customCharacteristics
$.ajax({
type: 'POST',
url: 'nrchkb/config',
dataType: 'json',
data: {customCharacteristics}
})
}
const uuidv4 = function () {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
const getRandomIntInclusive = function (min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1) + min)
}
const forbiddenPinCodes = [
'00000000',
'11111111',
'22222222',
'33333333',
'44444444',
'55555555',
'66666666',
'77777777',
'88888888',
'99999999',
'12345678',
'87654321'
]
const generatePinCode = function () {
const [a, b, c, d, e, f, g, h] = Array.from({length: 9}, () => getRandomIntInclusive(0, 9))
if (forbiddenPinCodes.includes(`${a}${b}${c}${d}${e}${f}${g}${h}`)) {
return generatePinCode()
}
return `${a}${b}${c}${d}-${e}${f}${g}${h}`
}
const validatePinCode = function (value) {
if (!value) {
return false
}
if (!RED.validators.regex(/([0-9]{3}-[0-9]{2}-[0-9]{3}|[0-9]{4}-[0-9]{4})/)(value)) {
return false
}
return !forbiddenPinCodes.includes(value.replaceAll('-', ''))
}
</script>
<script data-template-name="nrchkb" type="text/x-red">
<style>
ol#node-input-customCharacteristics-container .red-ui-typedInput-container {
flex:1;
}
.ui-slider .ui-slider-handle {
background: #ad1625;
}
</style>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> Custom Characteristics</label>
</div>
<div class="form-row node-input-customCharacteristics-container-row">
<ol id="node-input-customCharacteristics-container"></ol>
</div>
</script>