homey 4.2.3 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cmds/app/driver/firmware.mjs +36 -0
- package/lib/App.js +152 -0
- package/lib/DeviceFirmwareUpdatesHelper.mjs +387 -0
- package/lib/HomeyCompose.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import Log from '../../../../lib/Log.js';
|
|
5
|
+
import App from '../../../../lib/App.js';
|
|
6
|
+
|
|
7
|
+
export const desc = 'Add a device firmware update to a Driver';
|
|
8
|
+
|
|
9
|
+
export const builder = (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.option('driver', {
|
|
12
|
+
describe: 'Path to the driver to which the firmware update should be added',
|
|
13
|
+
type: 'string',
|
|
14
|
+
demandOption: true,
|
|
15
|
+
})
|
|
16
|
+
.option('firmware', {
|
|
17
|
+
describe: 'Path to a firmware file (can be specified multiple times)',
|
|
18
|
+
type: 'array',
|
|
19
|
+
demandOption: true,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
export const handler = async (yargs) => {
|
|
23
|
+
try {
|
|
24
|
+
const firmwareFiles = yargs.firmware.map((f) => path.resolve(process.cwd(), f));
|
|
25
|
+
|
|
26
|
+
const app = new App(yargs.path);
|
|
27
|
+
await app.createFirmwareUpdate({
|
|
28
|
+
driverPath: yargs.driver,
|
|
29
|
+
firmwareFiles,
|
|
30
|
+
});
|
|
31
|
+
process.exit(0);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
Log.error(err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
};
|
package/lib/App.js
CHANGED
|
@@ -3020,6 +3020,158 @@ $ sudo systemctl restart docker
|
|
|
3020
3020
|
Log.success(`Flow created in \`${flowPath}\``);
|
|
3021
3021
|
}
|
|
3022
3022
|
|
|
3023
|
+
async createFirmwareUpdate({ driverPath, firmwareFiles } = {}) {
|
|
3024
|
+
if (App.hasHomeyCompose({ appPath: this.path }) === false) {
|
|
3025
|
+
// Note: this checks that we are in a valid homey app folder
|
|
3026
|
+
App.getManifest({ appPath: this.path });
|
|
3027
|
+
|
|
3028
|
+
if (await this._askComposeMigration()) {
|
|
3029
|
+
await this.migrateToCompose();
|
|
3030
|
+
} else {
|
|
3031
|
+
throw new Error('This command requires Homey compose, run `homey app compose` to migrate!');
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// Load driver
|
|
3036
|
+
if (!driverPath) {
|
|
3037
|
+
throw new Error('A driver path is required. Use --driver to specify the path to the driver.');
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
driverPath = path.isAbsolute(driverPath) ? driverPath : path.resolve(this.path, driverPath);
|
|
3041
|
+
const selectedDriverId = path.basename(driverPath);
|
|
3042
|
+
|
|
3043
|
+
const driverJsonPath = path.join(driverPath, 'driver.compose.json');
|
|
3044
|
+
let driverJson;
|
|
3045
|
+
try {
|
|
3046
|
+
const driverJsonString = await readFileAsync(driverJsonPath, 'utf8');
|
|
3047
|
+
driverJson = JSON.parse(driverJsonString);
|
|
3048
|
+
} catch (err) {
|
|
3049
|
+
throw new Error(`Failed to load driver from \`${driverJsonPath}\`: ${err.message}`);
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
const isZigbeeDriver =
|
|
3053
|
+
driverJson.zigbee && typeof driverJson.zigbee === 'object' && driverJson.zigbee !== null;
|
|
3054
|
+
const isZwaveDriver =
|
|
3055
|
+
driverJson.zwave && typeof driverJson.zwave === 'object' && driverJson.zwave !== null;
|
|
3056
|
+
|
|
3057
|
+
if (!isZigbeeDriver && !isZwaveDriver) {
|
|
3058
|
+
throw new Error(
|
|
3059
|
+
`Driver \`${selectedDriverId}\` is not a Zigbee or Z-Wave driver. Firmware updates are only supported for Zigbee or Z-Wave drivers.`,
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// Dynamic import required: CJS cannot require() an ESM (.mjs) module
|
|
3064
|
+
const { default: DeviceFirmwareUpdatesHelper } =
|
|
3065
|
+
await import('./DeviceFirmwareUpdatesHelper.mjs');
|
|
3066
|
+
|
|
3067
|
+
// Validate firmware files
|
|
3068
|
+
if (!firmwareFiles || firmwareFiles.length === 0) {
|
|
3069
|
+
throw new Error(
|
|
3070
|
+
'At least one firmware file path is required. Use --firmware to specify the path to the firmware file.',
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
for (const firmwareFile of firmwareFiles) {
|
|
3074
|
+
await DeviceFirmwareUpdatesHelper.validateFirmwareFile({ firmwareFile, isZigbeeDriver });
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// Collect changelog and version constraints
|
|
3078
|
+
const { changelog, requireSpecificVersion } =
|
|
3079
|
+
await DeviceFirmwareUpdatesHelper.collectChangelog();
|
|
3080
|
+
|
|
3081
|
+
// Collect per-update Z-Wave constraints
|
|
3082
|
+
let applicableTo;
|
|
3083
|
+
if (requireSpecificVersion && isZwaveDriver) {
|
|
3084
|
+
applicableTo = await DeviceFirmwareUpdatesHelper.collectZwaveApplicableTo();
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// Collect device targeting (once per update)
|
|
3088
|
+
let device;
|
|
3089
|
+
if (isZigbeeDriver) {
|
|
3090
|
+
device = await DeviceFirmwareUpdatesHelper.collectZigbeeDevice({ driverJson });
|
|
3091
|
+
} else if (isZwaveDriver) {
|
|
3092
|
+
device = await DeviceFirmwareUpdatesHelper.collectZwaveDevice({ driverJson });
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// Collect Z-Wave version (once per update)
|
|
3096
|
+
let zwaveVersion;
|
|
3097
|
+
if (isZwaveDriver) {
|
|
3098
|
+
zwaveVersion = await DeviceFirmwareUpdatesHelper.collectZwaveVersion();
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
const files = [];
|
|
3102
|
+
for (const firmwareFile of firmwareFiles) {
|
|
3103
|
+
if (isZwaveDriver) {
|
|
3104
|
+
Log.info(`\nEntering details for: ${path.basename(firmwareFile)}`);
|
|
3105
|
+
}
|
|
3106
|
+
await DeviceFirmwareUpdatesHelper.copyFirmwareFile({
|
|
3107
|
+
firmwarePath: firmwareFile,
|
|
3108
|
+
appPath: this.path,
|
|
3109
|
+
selectedDriverId,
|
|
3110
|
+
});
|
|
3111
|
+
|
|
3112
|
+
let fileEntry;
|
|
3113
|
+
if (isZigbeeDriver) {
|
|
3114
|
+
// Collect per-file Zigbee version constraints
|
|
3115
|
+
let minFileVersion;
|
|
3116
|
+
let maxFileVersion;
|
|
3117
|
+
if (requireSpecificVersion) {
|
|
3118
|
+
({ minFileVersion, maxFileVersion } =
|
|
3119
|
+
await DeviceFirmwareUpdatesHelper.collectZigbeeVersionConstraints());
|
|
3120
|
+
}
|
|
3121
|
+
fileEntry = await DeviceFirmwareUpdatesHelper.collectZigbeeFileMetadata({
|
|
3122
|
+
firmwarePath: firmwareFile,
|
|
3123
|
+
minFileVersion,
|
|
3124
|
+
maxFileVersion,
|
|
3125
|
+
});
|
|
3126
|
+
} else {
|
|
3127
|
+
fileEntry = await DeviceFirmwareUpdatesHelper.collectZwaveFileMetadata({
|
|
3128
|
+
firmwarePath: firmwareFile,
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
files.push(fileEntry);
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// Build update JSON
|
|
3135
|
+
const updateJson = {
|
|
3136
|
+
changelog: { en: changelog },
|
|
3137
|
+
device,
|
|
3138
|
+
files,
|
|
3139
|
+
};
|
|
3140
|
+
|
|
3141
|
+
if (isZwaveDriver) {
|
|
3142
|
+
updateJson.version = zwaveVersion;
|
|
3143
|
+
updateJson.applicableTo = applicableTo;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// Write to driver.firmware.compose.json
|
|
3147
|
+
const updatesFilePath = path.join(
|
|
3148
|
+
this.path,
|
|
3149
|
+
'drivers',
|
|
3150
|
+
selectedDriverId,
|
|
3151
|
+
'driver.firmware.compose.json',
|
|
3152
|
+
);
|
|
3153
|
+
|
|
3154
|
+
const isNewFile = !fs.existsSync(updatesFilePath);
|
|
3155
|
+
const firmwareUpdatesJson = await DeviceFirmwareUpdatesHelper.readFirmwareUpdatesJson({
|
|
3156
|
+
updatesFilePath,
|
|
3157
|
+
});
|
|
3158
|
+
|
|
3159
|
+
if (isNewFile) {
|
|
3160
|
+
const { wakeUpInstruction } = await DeviceFirmwareUpdatesHelper.collectSleepMode();
|
|
3161
|
+
if (wakeUpInstruction) {
|
|
3162
|
+
firmwareUpdatesJson.wakeInstruction = { en: wakeUpInstruction };
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
firmwareUpdatesJson.updates.push(updateJson);
|
|
3167
|
+
await DeviceFirmwareUpdatesHelper.writeFirmwareUpdatesJson({
|
|
3168
|
+
updatesFilePath,
|
|
3169
|
+
firmwareUpdatesJson,
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
Log.success(`Firmware update created in \`${updatesFilePath}\``);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3023
3175
|
async createWidget() {
|
|
3024
3176
|
if (App.hasHomeyCompose({ appPath: this.path }) === false) {
|
|
3025
3177
|
// Note: this checks that we are in a valid homey app folder
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
|
|
5
|
+
import fse from 'fs-extra';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import semver from 'semver';
|
|
8
|
+
import homeyLib from 'homey-lib';
|
|
9
|
+
|
|
10
|
+
const HomeyLibUtil = homeyLib.Util;
|
|
11
|
+
|
|
12
|
+
const readFileAsync = promisify(fs.readFile);
|
|
13
|
+
const writeFileAsync = promisify(fs.writeFile);
|
|
14
|
+
const copyFileAsync = promisify(fs.copyFile);
|
|
15
|
+
|
|
16
|
+
export default class DeviceFirmwareUpdatesHelper {
|
|
17
|
+
static async validateFirmwareFile({ firmwareFile, isZigbeeDriver }) {
|
|
18
|
+
if (!fs.existsSync(firmwareFile)) {
|
|
19
|
+
throw new Error(`Firmware file \`${firmwareFile}\` does not exist!`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isZigbeeDriver) {
|
|
23
|
+
try {
|
|
24
|
+
await HomeyLibUtil.validateZigbeeOTAHeader({ filePath: firmwareFile });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error(`Invalid Zigbee OTA file: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return firmwareFile;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static async collectChangelog() {
|
|
34
|
+
return inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'string',
|
|
37
|
+
name: 'changelog',
|
|
38
|
+
message: 'What is the changelog for this firmware update?',
|
|
39
|
+
validate: (input) => input.length > 0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'confirm',
|
|
43
|
+
name: 'requireSpecificVersion',
|
|
44
|
+
message:
|
|
45
|
+
'Should this update only apply to devices within a certain firmware version range?',
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static async collectZigbeeVersionConstraints() {
|
|
52
|
+
const versions = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: 'string',
|
|
55
|
+
name: 'minFileVersion',
|
|
56
|
+
message: 'What is the minimum file version required on the device to perform the update?',
|
|
57
|
+
validate: (input) => {
|
|
58
|
+
input = Number(input);
|
|
59
|
+
|
|
60
|
+
if (Number.isNaN(input)) {
|
|
61
|
+
return 'Minimum file version must be a number';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!Number.isInteger(input)) {
|
|
65
|
+
return 'Minimum file version must be an integer';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (input < 0 || input > 0xffff_ffff) {
|
|
69
|
+
return 'Minimum file version must be a 32-bit unsigned integer';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'string',
|
|
77
|
+
name: 'maxFileVersion',
|
|
78
|
+
message: 'What is the maximum file version required on the device to perform the update?',
|
|
79
|
+
validate: (input) => {
|
|
80
|
+
input = Number(input);
|
|
81
|
+
|
|
82
|
+
if (Number.isNaN(input)) {
|
|
83
|
+
return 'Maximum file version must be a number';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!Number.isInteger(input)) {
|
|
87
|
+
return 'Maximum file version must be an integer';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (input < 0 || input > 0xffff_ffff) {
|
|
91
|
+
return 'Maximum file version must be a 32-bit unsigned integer';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
minFileVersion: Number(versions.minFileVersion),
|
|
101
|
+
maxFileVersion: Number(versions.maxFileVersion),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static async collectZwaveApplicableTo() {
|
|
106
|
+
const { applicableTo } = await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
type: 'string',
|
|
109
|
+
name: 'applicableTo',
|
|
110
|
+
message:
|
|
111
|
+
'Enter a semver constraint to specify which device firmware versions this update should apply to (e.g. <2.0.0)',
|
|
112
|
+
validate: (input) => {
|
|
113
|
+
const validated = semver.validRange(input);
|
|
114
|
+
return validated ? true : 'Please enter a valid semver constraint';
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
return applicableTo;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static async collectZigbeeDevice({ driverJson }) {
|
|
122
|
+
const manufacturerNames = Array.isArray(driverJson.zigbee.manufacturerName)
|
|
123
|
+
? driverJson.zigbee.manufacturerName
|
|
124
|
+
: [driverJson.zigbee.manufacturerName];
|
|
125
|
+
const productIds = Array.isArray(driverJson.zigbee.productId)
|
|
126
|
+
? driverJson.zigbee.productId
|
|
127
|
+
: [driverJson.zigbee.productId];
|
|
128
|
+
|
|
129
|
+
const { selectedManufacturerNames, selectedProductIds } = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: 'checkbox',
|
|
132
|
+
name: 'selectedManufacturerNames',
|
|
133
|
+
message: 'Which manufacturer names should this firmware update apply to?',
|
|
134
|
+
choices: manufacturerNames,
|
|
135
|
+
default: manufacturerNames,
|
|
136
|
+
validate: (input) => {
|
|
137
|
+
return input.length > 0 || 'Select at least one manufacturer name';
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'checkbox',
|
|
142
|
+
name: 'selectedProductIds',
|
|
143
|
+
message: 'Which product IDs should this firmware update apply to?',
|
|
144
|
+
choices: productIds,
|
|
145
|
+
default: productIds,
|
|
146
|
+
validate: (input) => {
|
|
147
|
+
return input.length > 0 || 'Select at least one product ID';
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
manufacturerName:
|
|
154
|
+
selectedManufacturerNames.length === 1
|
|
155
|
+
? selectedManufacturerNames[0]
|
|
156
|
+
: selectedManufacturerNames,
|
|
157
|
+
productId: selectedProductIds.length === 1 ? selectedProductIds[0] : selectedProductIds,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static async collectZwaveDevice({ driverJson }) {
|
|
162
|
+
const toArray = (value) => {
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
return value;
|
|
165
|
+
} else if (value !== undefined) {
|
|
166
|
+
return [value];
|
|
167
|
+
} else {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const manufacturerIds = toArray(driverJson.zwave.manufacturerId);
|
|
173
|
+
const productTypeIds = toArray(driverJson.zwave.productTypeId);
|
|
174
|
+
const productIds = toArray(driverJson.zwave.productId);
|
|
175
|
+
|
|
176
|
+
const { selectedManufacturerIds, selectedProductTypeIds, selectedProductIds } =
|
|
177
|
+
await inquirer.prompt([
|
|
178
|
+
{
|
|
179
|
+
type: 'checkbox',
|
|
180
|
+
name: 'selectedManufacturerIds',
|
|
181
|
+
message: 'Which manufacturer IDs should this firmware update apply to?',
|
|
182
|
+
choices: manufacturerIds,
|
|
183
|
+
default: manufacturerIds,
|
|
184
|
+
validate: (input) => {
|
|
185
|
+
return input.length > 0 || 'Select at least one manufacturer ID';
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: 'checkbox',
|
|
190
|
+
name: 'selectedProductTypeIds',
|
|
191
|
+
message: 'Which product type IDs should this firmware update apply to?',
|
|
192
|
+
choices: productTypeIds,
|
|
193
|
+
default: productTypeIds,
|
|
194
|
+
validate: (input) => {
|
|
195
|
+
return input.length > 0 || 'Select at least one product type ID';
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'checkbox',
|
|
200
|
+
name: 'selectedProductIds',
|
|
201
|
+
message: 'Which product IDs should this firmware update apply to?',
|
|
202
|
+
choices: productIds,
|
|
203
|
+
default: productIds,
|
|
204
|
+
validate: (input) => {
|
|
205
|
+
return input.length > 0 || 'Select at least one product ID';
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
manufacturerId:
|
|
212
|
+
selectedManufacturerIds.length === 1 ? selectedManufacturerIds[0] : selectedManufacturerIds,
|
|
213
|
+
productTypeId:
|
|
214
|
+
selectedProductTypeIds.length === 1 ? selectedProductTypeIds[0] : selectedProductTypeIds,
|
|
215
|
+
productId: selectedProductIds.length === 1 ? selectedProductIds[0] : selectedProductIds,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static async collectZigbeeFileMetadata({ firmwarePath, minFileVersion, maxFileVersion }) {
|
|
220
|
+
const header = await HomeyLibUtil.parseZigbeeOTAHeader(firmwarePath);
|
|
221
|
+
const integrity = await HomeyLibUtil.getIntegrity(firmwarePath, 'sha256');
|
|
222
|
+
const fileName = path.basename(firmwarePath);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
fileVersion: header.fileVersion,
|
|
226
|
+
imageType: header.imageType,
|
|
227
|
+
manufacturerCode: header.manufacturerCode,
|
|
228
|
+
minFileVersion,
|
|
229
|
+
maxFileVersion,
|
|
230
|
+
maxHardwareVersion: header.maximumHardwareVersion,
|
|
231
|
+
minHardwareVersion: header.minimumHardwareVersion,
|
|
232
|
+
size: header.totalImageSize,
|
|
233
|
+
name: fileName,
|
|
234
|
+
integrity,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static async collectZwaveFileMetadata({ firmwarePath }) {
|
|
239
|
+
const { targetId, region } = await inquirer.prompt([
|
|
240
|
+
{
|
|
241
|
+
type: 'number',
|
|
242
|
+
name: 'targetId',
|
|
243
|
+
message: 'What is the chip target ID for this file?',
|
|
244
|
+
default: 0,
|
|
245
|
+
validate: (input) => {
|
|
246
|
+
if (Number.isNaN(input)) {
|
|
247
|
+
return 'Target ID must be a number';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!Number.isInteger(input)) {
|
|
251
|
+
return 'Target ID must be an integer';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (input < 0 || input > 255) {
|
|
255
|
+
return 'Target ID must be between 0 and 255';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return true;
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: 'list',
|
|
263
|
+
name: 'region',
|
|
264
|
+
message: 'What is the region for this file?',
|
|
265
|
+
choices: [
|
|
266
|
+
{ name: 'None/Global', value: null },
|
|
267
|
+
{ name: 'ANZ - Australia/New Zealand (919.8 MHz / 921.4 MHz)', value: 'ANZ' },
|
|
268
|
+
{ name: 'CN - China (868.4 MHz)', value: 'CN' },
|
|
269
|
+
{ name: 'EU - Europe (868.4 MHz / 869.85 MHz)', value: 'EU' },
|
|
270
|
+
{ name: 'HK - Hong Kong (919.8 MHz)', value: 'HK' },
|
|
271
|
+
{ name: 'IL - Israel (916 MHz)', value: 'IL' },
|
|
272
|
+
{ name: 'IN - India (865.2 MHz)', value: 'IN' },
|
|
273
|
+
{ name: 'JP - Japan (922.5 MHz / 923.9 MHz / 926.3 MHz)', value: 'JP' },
|
|
274
|
+
{ name: 'KR - Korea (920.9 MHz / 921.7 MHz / 923.1 MHz)', value: 'KR' },
|
|
275
|
+
{ name: 'RU - Russia (869 MHz)', value: 'RU' },
|
|
276
|
+
// { name: 'US_LR - United States of America (Z-Wave & Long Range)', value: 'US_LR' },
|
|
277
|
+
{ name: 'US - United States of America (908.4 MHz / 916 MHz)', value: 'US' },
|
|
278
|
+
],
|
|
279
|
+
default: null,
|
|
280
|
+
},
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
const integrity = await HomeyLibUtil.getIntegrity(firmwarePath, 'sha256');
|
|
284
|
+
const fileName = path.basename(firmwarePath);
|
|
285
|
+
const { size } = await fs.promises.stat(firmwarePath);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
name: fileName,
|
|
289
|
+
integrity,
|
|
290
|
+
size,
|
|
291
|
+
targetId,
|
|
292
|
+
region: region !== null ? region : undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static async collectZwaveVersion() {
|
|
297
|
+
const { version } = await inquirer.prompt([
|
|
298
|
+
{
|
|
299
|
+
type: 'string',
|
|
300
|
+
name: 'version',
|
|
301
|
+
message: 'What is the version of the firmware update?',
|
|
302
|
+
validate: (input) => {
|
|
303
|
+
if (input.length === 0) {
|
|
304
|
+
return 'Version cannot be empty';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!semver.valid(input)) {
|
|
308
|
+
return 'Please enter a valid semver version';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return true;
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
]);
|
|
315
|
+
return version;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
static async copyFirmwareFile({ firmwarePath, appPath, selectedDriverId }) {
|
|
319
|
+
const fileName = path.basename(firmwarePath);
|
|
320
|
+
const firmwareDestPath = path.join(
|
|
321
|
+
appPath,
|
|
322
|
+
'drivers',
|
|
323
|
+
selectedDriverId,
|
|
324
|
+
'assets',
|
|
325
|
+
'firmware',
|
|
326
|
+
fileName,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
await fse.ensureDir(path.dirname(firmwareDestPath));
|
|
330
|
+
await copyFileAsync(firmwarePath, firmwareDestPath);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static async collectSleepMode() {
|
|
334
|
+
const { hasSleepMode } = await inquirer.prompt([
|
|
335
|
+
{
|
|
336
|
+
type: 'confirm',
|
|
337
|
+
name: 'hasSleepMode',
|
|
338
|
+
message: 'Does this device go into a sleep mode?',
|
|
339
|
+
default: false,
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
if (!hasSleepMode) {
|
|
344
|
+
return { hasSleepMode };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { wakeUpInstruction } = await inquirer.prompt([
|
|
348
|
+
{
|
|
349
|
+
type: 'string',
|
|
350
|
+
name: 'wakeUpInstruction',
|
|
351
|
+
message: 'Enter a short description on how to wake the device from sleep mode.',
|
|
352
|
+
validate: (input) => input.length > 0,
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
return { hasSleepMode, wakeUpInstruction };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
static async readFirmwareUpdatesJson({ updatesFilePath }) {
|
|
360
|
+
try {
|
|
361
|
+
const content = await readFileAsync(updatesFilePath, 'utf8');
|
|
362
|
+
const json = JSON.parse(content);
|
|
363
|
+
|
|
364
|
+
if (
|
|
365
|
+
typeof json !== 'object' ||
|
|
366
|
+
json === null ||
|
|
367
|
+
!json.updates ||
|
|
368
|
+
!Array.isArray(json.updates)
|
|
369
|
+
) {
|
|
370
|
+
throw new Error('Invalid firmware updates JSON: missing "updates" array');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return json;
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err.code === 'ENOENT') {
|
|
376
|
+
return { updates: [] };
|
|
377
|
+
}
|
|
378
|
+
throw new Error(
|
|
379
|
+
`Error in \`driver.firmware.compose.json\` at \`${updatesFilePath}\`: ${err.message}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static async writeFirmwareUpdatesJson({ updatesFilePath, firmwareUpdatesJson }) {
|
|
385
|
+
await writeFileAsync(updatesFilePath, JSON.stringify(firmwareUpdatesJson, null, 2));
|
|
386
|
+
}
|
|
387
|
+
}
|
package/lib/HomeyCompose.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
/drivers/<id>/driver.compose.json (extend with "$extends": [ "<template_id>" ])
|
|
20
20
|
/drivers/<id>/driver.settings.compose.json
|
|
21
21
|
(array with driver settings, extend with "$extends": "<template_id>"))
|
|
22
|
+
/drivers/<id>/driver.firmware.compose.json
|
|
22
23
|
/drivers/<id>/driver.flow.compose.json (object with flow cards, device arg is added automatically)
|
|
23
24
|
/drivers/<id>/driver.pair.compose.json (object with pair views)
|
|
24
25
|
/drivers/<id>/driver.repair.compose.json (object with repair views)
|
|
@@ -220,6 +221,15 @@ class HomeyCompose {
|
|
|
220
221
|
if (err.code !== 'ENOENT') throw new Error(err);
|
|
221
222
|
}
|
|
222
223
|
|
|
224
|
+
// merge firmware updates
|
|
225
|
+
try {
|
|
226
|
+
driverJson.firmwareUpdates = await this._getJsonFile(
|
|
227
|
+
path.join(this._appPath, 'drivers', driverId, 'driver.firmware.compose.json'),
|
|
228
|
+
);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err.code !== 'ENOENT') throw new Error(err);
|
|
231
|
+
}
|
|
232
|
+
|
|
223
233
|
// merge template settings
|
|
224
234
|
try {
|
|
225
235
|
const settingsTemplates = await this._getJsonFiles(
|