UNPKG

hb-deconz-tools

Version:
218 lines (196 loc) 6.37 kB
// hb-deconz-tools/lib/OtauTool.js // // Homebridge deCONZ Tools. // Copyright © 2023-2026 Erik Baauw. All rights reserved. import { createHash } from 'node:crypto' import { writeFile } from 'node:fs/promises' import { toHexString } from 'hb-lib-tools' import { CommandLineParser } from 'hb-lib-tools/CommandLineParser' import { CommandLineTool } from 'hb-lib-tools/CommandLineTool' import { HttpClient } from 'hb-lib-tools/HttpClient' import { OptionParser } from 'hb-lib-tools/OptionParser' import { OtauImage } from 'hb-deconz-tools/OtauImage' const { b, u } = CommandLineTool const usage = `${b('otau')} [${b('-hVD')}] [${b('-t')} ${u('timeout')}]` // const usage = `${b('otau')} [${b('-hVD')}] [${b('-t')} ${u('timeout')}] [${u('command')}] [${u('parameter')}...]` const help = `Handle Zigbee OTAU files. Usage: ${usage} Download OTAU files from the ${b('zigbee-OTA')} repository into the current directory, using the file names as expected by the deCONZ OTAU plugin. Parameters: ${b('-h')}, ${b('--help')} Print this help and exit. ${b('-V')}, ${b('--version')} Print version and exit. ${b('-D')}, ${b('--debug')} Print debug messages. ${b('-t')} ${u('timeout')}, ${b('--timeout=')}${u('timeout')} Set timeout to ${u('timeout')} seconds instead of default ${b(5)}.` class OtauTool extends CommandLineTool { constructor (pkgJson) { super() this.usage = usage this.options = { timeout: 5 } this.pkgJson = pkgJson } parseArguments () { const parser = new CommandLineParser(this.pkgJson) parser .help('h', 'help', help) .version('V', 'version') .debug('D', 'debug', this) .option('t', 'timeout', (value) => { this.options.timeout = OptionParser.toInt( 'timeout', value, 1, 60, true ) }) .remaining((list) => { this.fileList = list }) .parse() } async main () { try { this.parseArguments() await this.list() } catch (error) { await this.fatal(error) } } createClient (params) { const options = { https: true, logger: this, maxSockets: 10, timeout: this.options.timeout, validStatusCodes: [200, 302] } Object.assign(options, params) const client = new HttpClient(options) return client } validate (image) { const hash = createHash('sha512') hash.update(image.body) if (image.body.length !== image.fileSize) { this.warn('%s: size mismatch', image.fileName) } if (hash.digest('hex') !== image.sha512) { this.warn('%s: checksum error', image.fileName) } try { image.image = new OtauImage(image.body) } catch (error) { this.warn('%s: %s', image.fileName, error) return } if (image.image.manufacturerCode !== image.manufacturerCode) { this.warn('%s: manufacturer code mismatch %j vs %j', image.fileName, image.image.manufacturerCode, image.manufacturerCode) } if (image.image.imageType !== image.imageType) { this.warn('%s: image type mismatch', image.fileName) } if (image.image.fileVersion !== image.fileVersion) { this.warn('%s: file version mismatch', image.fileName) } } async getImage (image) { let body if (image.url.startsWith(this.client.url)) { if (this.client == null) { this.client = this.createClient({ name: 'koenkk', host: 'raw.githubusercontent.com', path: '/Koenkk/zigbee-OTA/master' }) } const response = await this.client.get(image.url.slice(this.client.url.length)) body = response.body } else { const url = new URL(image.url) const client = this.createClient({ name: url.hostname, host: url.host }) const response = await client.get(url.pathname + url.search) if (response.statusCode === 302) { image.url = response.headers.location return this.getImage(image) } body = response.body } image.body = body this.validate(image) return body } async downloadImage (image) { if (this.files[image.fileName] != null && this.files[image.fileName] !== image.url) { this.warn('%s: duplicate filename', image.fileName, image.modelId) return } this.files[image.fileName] = image.url try { this.debug('%s: downloading from %s...', image.fileName, image.url) await this.getImage(image) await writeFile(image.fileName, image.body) this.debug('%s: download OK', image.fileName) } catch (error) { if (!(error instanceof HttpClient.HttpError)) { this.error(error) } } } enrich (image) { image.manufacturerCodeHex = toHexString(image.manufacturerCode, 4) image.imageTypeHex = toHexString(image.imageType, 4) image.fileVersionHex = toHexString(image.fileVersion, 8) const fileNameElements = [ image.manufacturerCodeHex, image.imageTypeHex, image.fileVersionHex ] if (image.duplicate) { this.warn( '%s-%s-%s: %j: duplicate image', image.manufacturerCodeHex, image.imageTypeHex, image.fileVersionHex, image.modelId ) fileNameElements.push(image.modelId) } image.fileName = fileNameElements.join('-') + '.zigbee' } async list () { this.client = this.createClient({ name: 'koenkk', host: 'raw.githubusercontent.com', path: '/Koenkk/zigbee-OTA/master' }) this.client.setMaxListeners(Infinity) const { body } = await this.client.get('/index.json') await writeFile('index.json', body) const images = JSON.parse(body) const sortedImages = images.sort((a, b) => { if (a.manufacturerCode !== b.manufacturerCode) { return a.manufacturerCode - b.manufacturerCode } if (a.imageType !== b.imageType) { return a.imageType - b.imageType } if (a.fileVersion !== b.fileVersion) { return a.fileVersion - b.fileVersion } a.duplicate = true b.duplicate = true return a.modelId - b.modelId }) this.debug('koenkk: found %d images', images.length) this.files = [] const jobs = [] for (const image of sortedImages) { this.enrich(image) jobs.push(this.downloadImage(image)) } for (const job of jobs) { await job } } } export { OtauTool }