mirror of
https://github.com/Koenkk/zigbee-OTA.git
synced 2026-10-13 17:53:53 +00:00
Automate via workflows. Add auto-archiving for downgrade. (#581)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import {getJson, getLatestImage, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type ReleaseAssetJson = {
|
||||
url: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
label: null;
|
||||
uploader: Record<string, unknown>;
|
||||
content_type: string;
|
||||
state: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
browser_download_url: string;
|
||||
};
|
||||
type ReleaseJson = {
|
||||
url: string;
|
||||
assets_url: string;
|
||||
upload_url: string;
|
||||
html_url: string;
|
||||
id: number;
|
||||
author: Record<string, unknown>;
|
||||
node_id: string;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
draft: false;
|
||||
prerelease: false;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
assets: ReleaseAssetJson[];
|
||||
tarball_url: string;
|
||||
zipball_url: string;
|
||||
body: string;
|
||||
reactions: Record<string, unknown>;
|
||||
};
|
||||
type ReleasesJson = ReleaseJson[];
|
||||
type AssetFindPredicate = (value: ReleaseAssetJson, index: number, obj: ReleaseAssetJson[]) => unknown;
|
||||
|
||||
function sortByPublishedAt(a: ReleaseJson, b: ReleaseJson): number {
|
||||
return a.published_at < b.published_at ? -1 : a.published_at > b.published_at ? 1 : 0;
|
||||
}
|
||||
|
||||
function isDifferent(newData: ReleaseAssetJson, cachedData?: ReleaseAssetJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.updated_at !== newData.updated_at;
|
||||
}
|
||||
|
||||
export async function writeCache(manufacturer: string, releasesUrl: string): Promise<void> {
|
||||
const releases = await getJson<ReleasesJson>(manufacturer, releasesUrl);
|
||||
|
||||
if (releases?.length) {
|
||||
writeCacheJson(manufacturer, releases);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(manufacturer: string, releasesUrl: string, assetFinders: AssetFindPredicate[]): Promise<void> {
|
||||
const logPrefix = `[${manufacturer}]`;
|
||||
const releases = await getJson<ReleasesJson>(manufacturer, releasesUrl);
|
||||
|
||||
if (releases?.length) {
|
||||
const release = getLatestImage(releases, sortByPublishedAt);
|
||||
|
||||
if (release) {
|
||||
const cachedData = readCacheJson<ReleasesJson | undefined>(manufacturer);
|
||||
const cached = cachedData?.length ? getLatestImage(cachedData, sortByPublishedAt) : undefined;
|
||||
|
||||
for (const assetFinder of assetFinders) {
|
||||
const asset = release.assets.find(assetFinder);
|
||||
|
||||
if (asset) {
|
||||
const cachedAsset = cached?.assets.find(assetFinder);
|
||||
|
||||
if (!isDifferent(asset, cachedAsset)) {
|
||||
console.log(`[${manufacturer}:${asset.name}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await processFirmwareImage(manufacturer, asset.name, asset.browser_download_url, {
|
||||
manufacturerName: [manufacturer],
|
||||
releaseNotes: release.html_url,
|
||||
});
|
||||
} else {
|
||||
console.error(`${logPrefix} No image found.`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`${logPrefix} No release found.`);
|
||||
}
|
||||
|
||||
writeCacheJson(manufacturer, releases);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {getJson, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type ImagesJsonBuildPart = {
|
||||
path: string; // .bin
|
||||
offset: number;
|
||||
type?: 'app' | 'storage';
|
||||
ota?: string; // .ota
|
||||
};
|
||||
type ImagesJsonBuild = {
|
||||
chipFamily: string;
|
||||
target: string;
|
||||
parts: ImagesJsonBuildPart[];
|
||||
};
|
||||
type ImagesJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
home_assistant_domain: string;
|
||||
funding_url: string;
|
||||
new_install_prompt_erase: boolean;
|
||||
builds: ImagesJsonBuild[];
|
||||
};
|
||||
|
||||
const NAME = 'Gmmts';
|
||||
// const LOG_PREFIX = `[${NAME}]`;
|
||||
const BASE_URL = 'https://update.gammatroniques.fr/';
|
||||
const MANIFEST_URL_PATH = `/manifest.json`;
|
||||
const MODEL_IDS = ['ticmeter'];
|
||||
|
||||
function isDifferent(newData: ImagesJson, cachedData?: ImagesJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.version !== newData.version;
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
for (const modelId of MODEL_IDS) {
|
||||
const url = `${BASE_URL}${modelId}${MANIFEST_URL_PATH}`;
|
||||
const page = await getJson<ImagesJson>(NAME, url);
|
||||
|
||||
if (page?.builds?.length) {
|
||||
writeCacheJson(`${NAME}_${modelId}`, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
for (const modelId of MODEL_IDS) {
|
||||
const logPrefix = `[${NAME}:${modelId}]`;
|
||||
const url = `${BASE_URL}${modelId}${MANIFEST_URL_PATH}`;
|
||||
const page = await getJson<ImagesJson>(NAME, url);
|
||||
|
||||
if (!page?.builds?.length) {
|
||||
console.error(`${logPrefix} No image data.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cacheFileName = `${NAME}_${modelId}`;
|
||||
|
||||
if (!isDifferent(page, readCacheJson(cacheFileName))) {
|
||||
console.log(`${logPrefix} No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
writeCacheJson(cacheFileName, page);
|
||||
|
||||
const appUrl: ImagesJsonBuildPart | undefined = page.builds[0].parts.find((part) => part.type === 'app');
|
||||
|
||||
if (!appUrl || !appUrl.ota) {
|
||||
console.error(`${logPrefix} No image found.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const firmwareFileName = appUrl.ota.split('/').pop()!;
|
||||
|
||||
await processFirmwareImage(NAME, firmwareFileName, appUrl.ota, {manufacturerName: [NAME]});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import {getJson, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type GatewayImageJson = {
|
||||
fw_binary_url: string;
|
||||
fw_filesize: number;
|
||||
fw_hotfix_version: number;
|
||||
fw_major_version: number;
|
||||
fw_minor_version: number;
|
||||
fw_req_hotfix_version: number;
|
||||
fw_req_major_version: number;
|
||||
fw_req_minor_version: number;
|
||||
fw_type: 0;
|
||||
fw_update_prio: number;
|
||||
fw_weblink_relnote: string;
|
||||
};
|
||||
type DeviceImageJson = {
|
||||
fw_binary_url: string;
|
||||
fw_file_version_LSB: number;
|
||||
fw_file_version_MSB: number;
|
||||
fw_filesize: number;
|
||||
fw_image_type: number;
|
||||
fw_manufacturer_id: number;
|
||||
fw_type: 2;
|
||||
};
|
||||
type ImagesJson = (GatewayImageJson | DeviceImageJson)[];
|
||||
|
||||
const NAME = 'IKEA';
|
||||
const LOG_PREFIX = `[${NAME}]`;
|
||||
const PRODUCTION_FIRMWARE_URL = 'http://fw.ota.homesmart.ikea.net/feed/version_info.json';
|
||||
// const TEST_FIRMWARE_URL = 'http://fw.test.ota.homesmart.ikea.net/feed/version_info.json';
|
||||
export const RELEASE_NOTES_URL = 'https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html';
|
||||
|
||||
function findInCache(image: DeviceImageJson, cachedData?: ImagesJson): DeviceImageJson | undefined {
|
||||
// `fw_type` compare ensures always `DeviceImagesJson`
|
||||
return cachedData?.find(
|
||||
(d) => d.fw_type == image.fw_type && d.fw_image_type == image.fw_image_type && d.fw_manufacturer_id == image.fw_manufacturer_id,
|
||||
) as DeviceImageJson | undefined;
|
||||
}
|
||||
|
||||
function isDifferent(newData: DeviceImageJson, cachedData?: DeviceImageJson): boolean {
|
||||
return (
|
||||
Boolean(process.env.IGNORE_CACHE) ||
|
||||
!cachedData ||
|
||||
cachedData.fw_file_version_LSB !== newData.fw_file_version_LSB ||
|
||||
cachedData.fw_file_version_MSB !== newData.fw_file_version_MSB
|
||||
);
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, PRODUCTION_FIRMWARE_URL);
|
||||
|
||||
if (images?.length) {
|
||||
writeCacheJson(NAME, images);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, PRODUCTION_FIRMWARE_URL);
|
||||
|
||||
if (images?.length) {
|
||||
const cachedData = readCacheJson<ImagesJson | undefined>(NAME);
|
||||
|
||||
for (const image of images) {
|
||||
if (image.fw_type !== 2) {
|
||||
// ignore gateway firmware
|
||||
continue;
|
||||
}
|
||||
|
||||
const firmwareFileName = image.fw_binary_url.split('/').pop()!;
|
||||
|
||||
if (!isDifferent(image, findInCache(image, cachedData))) {
|
||||
console.log(`[${NAME}:${firmwareFileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await processFirmwareImage(NAME, firmwareFileName, image.fw_binary_url, {manufacturerName: [NAME], releaseNotes: RELEASE_NOTES_URL});
|
||||
}
|
||||
|
||||
writeCacheJson(NAME, images);
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {getJson, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
import {RELEASE_NOTES_URL} from './ikea.js';
|
||||
|
||||
type GatewayImageJson = {
|
||||
fw_type: 3;
|
||||
fw_sha3_256: string;
|
||||
fw_binary_url: string;
|
||||
fw_update_prio: number;
|
||||
fw_filesize: number;
|
||||
fw_minor_version: number;
|
||||
fw_major_version: number;
|
||||
fw_hotfix_version: number;
|
||||
fw_binary_checksum: string;
|
||||
};
|
||||
type DeviceImageJson = {
|
||||
fw_image_type: number;
|
||||
fw_type: 2;
|
||||
fw_sha3_256: string;
|
||||
fw_binary_url: string;
|
||||
};
|
||||
|
||||
type ImagesJson = (GatewayImageJson | DeviceImageJson)[];
|
||||
|
||||
// same name as `ikea.ts` to keep everything in same folder
|
||||
const NAME = 'IKEA';
|
||||
const CACHE_FILENAME = `${NAME}_new`;
|
||||
const LOG_PREFIX = `[${NAME}_new]`;
|
||||
// requires cacerts/ikea_new.pem
|
||||
const FIRMWARE_URL = 'https://fw.ota.homesmart.ikea.com/check/update/prod';
|
||||
|
||||
function findInCache(image: DeviceImageJson, cachedData?: ImagesJson): DeviceImageJson | undefined {
|
||||
// `fw_type` compare ensures always `DeviceImagesJson`
|
||||
return cachedData?.find((d) => d.fw_type == image.fw_type && d.fw_image_type == image.fw_image_type) as DeviceImageJson | undefined;
|
||||
}
|
||||
|
||||
function isDifferent(newData: DeviceImageJson, cachedData?: DeviceImageJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.fw_sha3_256 !== newData.fw_sha3_256;
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (images?.length) {
|
||||
writeCacheJson(CACHE_FILENAME, images);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (images?.length) {
|
||||
const cachedData = readCacheJson<ImagesJson | undefined>(CACHE_FILENAME);
|
||||
|
||||
for (const image of images) {
|
||||
if (image.fw_type !== 2) {
|
||||
// ignore gateway firmware
|
||||
continue;
|
||||
}
|
||||
|
||||
const firmwareFileName = image.fw_binary_url.split('/').pop()!;
|
||||
|
||||
if (!isDifferent(image, findInCache(image, cachedData))) {
|
||||
console.log(`[${NAME}:${firmwareFileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await processFirmwareImage(NAME, firmwareFileName, image.fw_binary_url, {manufacturerName: [NAME], releaseNotes: RELEASE_NOTES_URL});
|
||||
}
|
||||
|
||||
writeCacheJson(CACHE_FILENAME, images);
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {getJson, getLatestImage, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type DeviceImageJson = {
|
||||
version: string;
|
||||
channel: 'beta' | 'production';
|
||||
firmware: string;
|
||||
manufacturer_id: number;
|
||||
image_type: number;
|
||||
};
|
||||
type ModelsJson = {
|
||||
[k: string]: DeviceImageJson[];
|
||||
};
|
||||
|
||||
const NAME = 'Inovelli';
|
||||
const LOG_PREFIX = `[${NAME}]`;
|
||||
const FIRMWARE_URL = 'https://files.inovelli.com/firmware/firmware.json';
|
||||
|
||||
function sortByVersion(a: DeviceImageJson, b: DeviceImageJson): number {
|
||||
const aRadix = a.version.match(/[a-fA-F]/) ? 16 : 10;
|
||||
const bRadix = b.version.match(/[a-fA-F]/) ? 16 : 10;
|
||||
const aVersion = parseInt(a.version, aRadix);
|
||||
const bVersion = parseInt(b.version, bRadix);
|
||||
|
||||
return aVersion < bVersion ? -1 : aVersion > bVersion ? 1 : 0;
|
||||
}
|
||||
|
||||
function isDifferent(newData: DeviceImageJson, cachedData?: DeviceImageJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.version !== newData.version;
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
const models = await getJson<ModelsJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (models) {
|
||||
writeCacheJson(NAME, models);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
const models = await getJson<ModelsJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (models) {
|
||||
const cachedData = readCacheJson<ModelsJson | undefined>(NAME);
|
||||
|
||||
for (const model in models) {
|
||||
if (model == '') {
|
||||
// ignore empty key (bug)
|
||||
continue;
|
||||
}
|
||||
|
||||
const image = getLatestImage(models[model], sortByVersion);
|
||||
|
||||
if (!image) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firmwareFileName = image.firmware.split('/').pop()!;
|
||||
|
||||
if (cachedData && !isDifferent(image, getLatestImage(cachedData[model], sortByVersion))) {
|
||||
console.log(`[${NAME}:${firmwareFileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await processFirmwareImage(NAME, firmwareFileName, image.firmware, {manufacturerName: [NAME]});
|
||||
}
|
||||
|
||||
writeCacheJson(NAME, models);
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import {getJson, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type ImageJson = {
|
||||
vendor: string;
|
||||
vendor_name: string;
|
||||
device: string;
|
||||
device_name: string;
|
||||
platform: string;
|
||||
platform_name: string;
|
||||
latest_firmware: {
|
||||
release: {
|
||||
version: string;
|
||||
date: string;
|
||||
images: {
|
||||
'zigbee.ota': {
|
||||
url: string;
|
||||
hash: string;
|
||||
filesize: number;
|
||||
};
|
||||
'zigbee.bin': {
|
||||
url: string;
|
||||
hash: string;
|
||||
filesize: number;
|
||||
};
|
||||
};
|
||||
changelog: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const NAME = 'Jethome';
|
||||
const LOG_PREFIX = `[${NAME}]`;
|
||||
const BASE_URL = 'https://fw.jethome.ru';
|
||||
const DEVICE_URL = `${BASE_URL}/api/devices/`;
|
||||
|
||||
const MODEL_IDS = ['WS7'];
|
||||
|
||||
function getCacheFileName(modelId: string): string {
|
||||
return `${NAME}_${modelId}`;
|
||||
}
|
||||
|
||||
function isDifferent(newData: ImageJson, cachedData?: ImageJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.latest_firmware.release.version !== newData.latest_firmware.release.version;
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
for (const modelId of MODEL_IDS) {
|
||||
const url = `${DEVICE_URL}${modelId}/info`;
|
||||
const image = await getJson<ImageJson>(NAME, url);
|
||||
|
||||
if (image?.latest_firmware?.release?.images) {
|
||||
writeCacheJson(getCacheFileName(modelId), image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
for (const modelId of MODEL_IDS) {
|
||||
const url = `${DEVICE_URL}${modelId}/info`;
|
||||
const image = await getJson<ImageJson>(NAME, url);
|
||||
|
||||
// XXX: this is assumed to always be present even for devices that support OTA but without images yet available?
|
||||
if (image?.latest_firmware?.release?.images) {
|
||||
const firmware = image.latest_firmware.release.images['zigbee.ota'];
|
||||
|
||||
if (!firmware) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firmwareUrl = BASE_URL + firmware.url;
|
||||
const firmwareFileName = firmwareUrl.split('/').pop()!;
|
||||
const cacheFileName = getCacheFileName(modelId);
|
||||
|
||||
if (!isDifferent(image, readCacheJson(cacheFileName))) {
|
||||
console.log(`[${NAME}:${firmwareFileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
writeCacheJson(cacheFileName, image);
|
||||
|
||||
await processFirmwareImage(NAME, firmwareFileName, firmwareUrl, {
|
||||
manufacturerName: [NAME],
|
||||
releaseNotes: BASE_URL + image.latest_firmware.release.changelog,
|
||||
});
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data for ${modelId}.`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import {getJson, getLatestImage, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage, ProcessFirmwareImageStatus} from '../process_firmware_image.js';
|
||||
|
||||
type FirmwareJson = {
|
||||
blob: null;
|
||||
identity: {
|
||||
company: number;
|
||||
product: number;
|
||||
version: {
|
||||
major: number;
|
||||
minor: number;
|
||||
build: number;
|
||||
revision: number;
|
||||
};
|
||||
};
|
||||
releaseNotes: string;
|
||||
/** Ledvance's API docs state the checksum should be `sha_256` but it is actually `shA256` */
|
||||
shA256: string;
|
||||
name: string;
|
||||
productName: string;
|
||||
/**
|
||||
* The fileVersion in hex is included in the fullName between the `/`, e.g.:
|
||||
* - PLUG COMPACT EU T/032b3674/PLUG_COMPACT_EU_T-0x00D6-0x032B3674-MF_DIS.OTA
|
||||
* - A19 RGBW/00102428/A19_RGBW_IMG0019_00102428-encrypted.ota
|
||||
*/
|
||||
fullName: string;
|
||||
extension: string;
|
||||
released: string;
|
||||
salesRegion: string;
|
||||
length: number;
|
||||
};
|
||||
type ImagesJson = {
|
||||
firmwares: FirmwareJson[];
|
||||
};
|
||||
type GroupedImagesJson = Record<string, FirmwareJson[]>;
|
||||
|
||||
const NAME = 'Ledvance';
|
||||
const LOG_PREFIX = `[${NAME}]`;
|
||||
const FIRMWARE_URL = 'https://api.update.ledvance.com/v1/zigbee/firmwares/';
|
||||
// const UPDATE_CHECK_URL = 'https://api.update.ledvance.com/v1/zigbee/firmwares/newer';
|
||||
// const UPDATE_CHECK_PARAMS = `?company=${manufCode}&product=${imageType}&version=0.0.0`;
|
||||
const UPDATE_DOWNLOAD_URL = 'https://api.update.ledvance.com/v1/zigbee/firmwares/download';
|
||||
/** XXX: getting 429 after a few downloads, force more throttling. Seems to trigger after around ~20 requests. */
|
||||
const FETCH_FAILED_THROTTLE_MS = 60000;
|
||||
const FETCH_FAILED_RETRIES = 3;
|
||||
|
||||
function groupByProduct(arr: FirmwareJson[]): GroupedImagesJson {
|
||||
return arr.reduce<GroupedImagesJson>((acc, cur) => {
|
||||
acc[cur.identity.product] = [...(acc[cur.identity.product] || []), cur];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function sortByReleased(a: FirmwareJson, b: FirmwareJson): number {
|
||||
return a.released < b.released ? -1 : a.released > b.released ? 1 : 0;
|
||||
}
|
||||
|
||||
function getVersionString(firmware: FirmwareJson): string {
|
||||
const {major, minor, build, revision} = firmware.identity.version;
|
||||
|
||||
return `${major}.${minor}.${build}.${revision}`;
|
||||
}
|
||||
|
||||
function isDifferent(newData: FirmwareJson, cachedData?: FirmwareJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || getVersionString(cachedData) !== getVersionString(newData);
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (images?.firmwares?.length) {
|
||||
writeCacheJson(NAME, images);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (images?.firmwares?.length) {
|
||||
const cachedData = readCacheJson<ImagesJson | undefined>(NAME);
|
||||
const cachedDataByProduct = cachedData?.firmwares?.length ? groupByProduct(cachedData.firmwares) : undefined;
|
||||
const firmwareByProduct = groupByProduct(images.firmwares);
|
||||
|
||||
for (const product in firmwareByProduct) {
|
||||
const firmware = getLatestImage(firmwareByProduct[product], sortByReleased);
|
||||
|
||||
if (!firmware) {
|
||||
console.error(`${LOG_PREFIX} No image found for ${product}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileVersionMatch = /\/(\d|\w+)\//.exec(firmware.fullName);
|
||||
|
||||
if (fileVersionMatch == null) {
|
||||
// ignore possible unsupported patterns
|
||||
continue;
|
||||
}
|
||||
|
||||
// const fileVersion = parseInt(fileVersionMatch[1], 16);
|
||||
const firmwareUrl = `${UPDATE_DOWNLOAD_URL}?company=${firmware.identity.company}&product=${firmware.identity.product}&version=${getVersionString(firmware)}`;
|
||||
const firmwareFileName = firmware.fullName.split('/').pop()!;
|
||||
|
||||
if (cachedDataByProduct && !isDifferent(firmware, getLatestImage(cachedDataByProduct[product], sortByReleased))) {
|
||||
console.log(`[${NAME}:${firmwareFileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < FETCH_FAILED_RETRIES; i++) {
|
||||
const status = await processFirmwareImage(NAME, firmwareFileName, firmwareUrl, {
|
||||
manufacturerName: [NAME],
|
||||
// workflow automatically computes sha512
|
||||
// sha256: firmware.shA256,
|
||||
releaseNotes: firmware.releaseNotes,
|
||||
});
|
||||
|
||||
if (status === ProcessFirmwareImageStatus.REQUEST_FAILED) {
|
||||
await new Promise((resolve) => setTimeout(resolve, FETCH_FAILED_THROTTLE_MS));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeCacheJson(NAME, images);
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as github from './github.js';
|
||||
|
||||
const NAME = 'Lixee';
|
||||
const FIRMWARE_URL = 'https://api.github.com/repos/fairecasoimeme/Zlinky_TIC/releases';
|
||||
/** @see https://github.com/fairecasoimeme/Zlinky_TIC?tab=readme-ov-file#route-or-limited-route-from-v7 */
|
||||
const FIRMWARE_EXT = '.ota';
|
||||
const FIRMWARE_LIMITED = `limited${FIRMWARE_EXT}`;
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
await github.writeCache(NAME, FIRMWARE_URL);
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
await github.download(NAME, FIRMWARE_URL, [(a): boolean => a.name.endsWith(FIRMWARE_EXT), (a): boolean => a.name.endsWith(FIRMWARE_LIMITED)]);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {getJson, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type ImageJson = {
|
||||
model: string;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
type ImagesJson = {
|
||||
versions: ImageJson[];
|
||||
};
|
||||
|
||||
const NAME = 'Salus';
|
||||
const LOG_PREFIX = `[${NAME}]`;
|
||||
const FIRMWARE_URL = 'https://eu.salusconnect.io/demo/default/status/firmware?timestamp=0';
|
||||
|
||||
function findInCache(image: ImageJson, cachedData?: ImagesJson): ImageJson | undefined {
|
||||
return cachedData?.versions?.find((d) => d.model == image.model);
|
||||
}
|
||||
|
||||
function isDifferent(newData: ImageJson, cachedData?: ImageJson): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.version !== newData.version;
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (images?.versions?.length) {
|
||||
writeCacheJson(NAME, images);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
const images = await getJson<ImagesJson>(NAME, FIRMWARE_URL);
|
||||
|
||||
if (images?.versions?.length) {
|
||||
const cachedData = readCacheJson<ImagesJson | undefined>(NAME);
|
||||
|
||||
for (const image of images.versions) {
|
||||
const archiveUrl = image.url; //.replace(/^http:\/\//, 'https://');
|
||||
const archiveFileName = archiveUrl.split('/').pop()!;
|
||||
|
||||
if (!isDifferent(image, findInCache(image, cachedData))) {
|
||||
console.log(`[${NAME}:${archiveFileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await processFirmwareImage(NAME, archiveFileName, archiveUrl, {manufacturerName: [NAME]}, true, (fileName) => fileName.endsWith('.ota'));
|
||||
}
|
||||
|
||||
writeCacheJson(NAME, images);
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import url from 'url';
|
||||
|
||||
import {getLatestImage, getText, readCacheJson, writeCacheJson} from '../common.js';
|
||||
import {processFirmwareImage} from '../process_firmware_image.js';
|
||||
|
||||
type Image = {
|
||||
fileName: string;
|
||||
imageType: string;
|
||||
hardwareVersionMin: number;
|
||||
hardwareVersionMax: number;
|
||||
fileVersion: number;
|
||||
};
|
||||
type GroupedImages = {
|
||||
[k: string]: Image[];
|
||||
};
|
||||
|
||||
const NAME = 'Ubisys';
|
||||
const LOG_PREFIX = `[${NAME}]`;
|
||||
const FIRMWARE_HTML_URL = 'http://fwu.ubisys.de/smarthome/OTA/release/index';
|
||||
|
||||
function groupByImageType(arr: Image[]): GroupedImages {
|
||||
return arr.reduce<GroupedImages>((acc, cur) => {
|
||||
acc[cur.imageType] = [...(acc[cur.imageType] || []), cur];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function sortByFileVersion(a: Image, b: Image): number {
|
||||
return a.fileVersion < b.fileVersion ? -1 : a.fileVersion > b.fileVersion ? 1 : 0;
|
||||
}
|
||||
|
||||
function isDifferent(newData: Image, cachedData?: Image): boolean {
|
||||
return Boolean(process.env.IGNORE_CACHE) || !cachedData || cachedData.fileVersion !== newData.fileVersion;
|
||||
}
|
||||
|
||||
function parseText(pageText: string): Image[] {
|
||||
const lines = pageText.split('\n');
|
||||
const images: Image[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// XXX: there are other images on the page that do not match this pattern
|
||||
const imageMatch = /10F2-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{8})\S*ota1?\.zigbee/gi.exec(line);
|
||||
|
||||
if (imageMatch != null) {
|
||||
images.push({
|
||||
fileName: imageMatch[0],
|
||||
imageType: imageMatch[1],
|
||||
hardwareVersionMin: parseInt(imageMatch[2], 16),
|
||||
hardwareVersionMax: parseInt(imageMatch[3], 16),
|
||||
fileVersion: parseInt(imageMatch[4], 16),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
const pageText = await getText(NAME, FIRMWARE_HTML_URL);
|
||||
|
||||
if (pageText?.length) {
|
||||
const images = parseText(pageText);
|
||||
|
||||
writeCacheJson(NAME, images);
|
||||
}
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
const pageText = await getText(NAME, FIRMWARE_HTML_URL);
|
||||
|
||||
if (pageText?.length) {
|
||||
const images = parseText(pageText);
|
||||
const imagesByType = groupByImageType(images);
|
||||
const cachedData = readCacheJson<Image[] | undefined>(NAME);
|
||||
const cachedDataByType = cachedData ? groupByImageType(cachedData) : undefined;
|
||||
|
||||
for (const imageType in imagesByType) {
|
||||
const image = getLatestImage(imagesByType[imageType], sortByFileVersion);
|
||||
|
||||
if (!image) {
|
||||
console.error(`${LOG_PREFIX} No image found for ${imageType}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cachedDataByType && !isDifferent(image, getLatestImage(cachedDataByType[imageType], sortByFileVersion))) {
|
||||
console.log(`[${NAME}:${image.fileName}] No change from last run.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE: removes `index` from url
|
||||
const firmwareUrl = url.resolve(FIRMWARE_HTML_URL, image.fileName);
|
||||
|
||||
await processFirmwareImage(NAME, image.fileName, firmwareUrl, {
|
||||
manufacturerName: [NAME],
|
||||
hardwareVersionMin: image.hardwareVersionMin,
|
||||
hardwareVersionMax: image.hardwareVersionMax,
|
||||
});
|
||||
}
|
||||
|
||||
writeCacheJson(NAME, images);
|
||||
} else {
|
||||
console.error(`${LOG_PREFIX} No image data.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as github from './github.js';
|
||||
|
||||
const NAME = 'Xyzroe';
|
||||
const FIRMWARE_URL = 'https://api.github.com/repos/xyzroe/ZigUSB_C6/releases';
|
||||
const FIRMWARE_EXT = '.ota';
|
||||
|
||||
export async function writeCache(): Promise<void> {
|
||||
await github.writeCache(NAME, FIRMWARE_URL);
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
await github.download(NAME, FIRMWARE_URL, [(a): boolean => a.name.endsWith(FIRMWARE_EXT)]);
|
||||
}
|
||||
Reference in New Issue
Block a user