diff --git a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/ZigBeeFirmwareProvider.java b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/ZigBeeFirmwareProvider.java index 912e2cc0f..c843320dc 100644 --- a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/ZigBeeFirmwareProvider.java +++ b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/ZigBeeFirmwareProvider.java @@ -13,12 +13,13 @@ package org.openhab.binding.zigbee.firmware; import java.io.File; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -47,20 +48,28 @@ public class ZigBeeFirmwareProvider implements FirmwareProvider { private Logger logger = LoggerFactory.getLogger(ZigBeeFirmwareProvider.class); - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - - private GithubLibraryReader directoryReader; + private Set directoryReaders = new HashSet<>(); @Activate - protected void activate() { + protected void activate() throws Exception { logger.debug("ZigBee Firmware Provider: Activated"); + String folder = OpenHAB.getUserDataFolder() + File.separator + "firmware" + File.separator; + GithubLibraryReader directoryReader; + directoryReader = new GithubLibraryReader(folder); try { - directoryReader.create("https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master"); - directoryReader.updateRemoteDirectory(); + directoryReader.create("https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json"); + directoryReaders.add(directoryReader); } catch (Exception e) { - logger.error("Exception activating ZigBee firmware provider ", e); + logger.error("Exception activating ZigBee upgrade firmware provider ", e); + } + directoryReader = new GithubLibraryReader(folder); + try { + directoryReader.create("https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index1.json"); + directoryReaders.add(directoryReader); + } catch (Exception e) { + logger.error("Exception activating ZigBee downgrade firmware provider ", e); } } @@ -76,20 +85,43 @@ protected void deactivate() { @Override public @Nullable Firmware getFirmware(@NonNull Thing thing, @NonNull String version, @Nullable Locale locale) { - ZigBeeFirmwareVersion requestedVersion = getRequestedVersionFromThing(thing); - if (requestedVersion == null) { + ZigBeeFirmwareVersion deviceVersion = getThingRequestedVersion(thing); + if (deviceVersion == null) { return null; } - List directory = directoryReader.getDirectory(); - for (DirectoryFileEntry firmware : directory) { - if (firmware.getFirmwareVersion().equals(requestedVersion)) { - return getZigBeeFirmware(thing.getThingTypeUID(), firmware); + int specificVersion; + try { + specificVersion = Integer.parseInt(version); + } catch (NumberFormatException e) { + logger.info("ZigBee Firmware Provider: Requested version {} for {} is not an integer", version, + thing.getUID()); + return null; + } + logger.debug("ZigBee Firmware Provider: Getting version {} of {}", specificVersion, thing.getUID()); + + GithubLibraryReader directoryReader = null; + DirectoryFileEntry directory = null; + for (GithubLibraryReader reader : directoryReaders) { + directory = reader.getDirectoryEntry(deviceVersion, specificVersion); + if (directory != null) { + logger.debug("ZigBee Firmware Provider: Firmware available from {}", reader.getRepositoryAddress()); + directoryReader = reader; + break; } } - logger.debug("Unable to find firmware version {}", version); - return null; + if (directory == null || directoryReader == null) { + logger.debug("ZigBee Firmware Provider: Firmware not found"); + return null; + } + + InputStream inputStream = directoryReader.getInputStream(directory); + if (inputStream == null) { + return null; + } + + return getZigBeeFirmware(thing.getThingTypeUID(), directory, inputStream); } @Override @@ -99,34 +131,48 @@ protected void deactivate() { @Override public @Nullable Set<@NonNull Firmware> getFirmwares(@NonNull Thing thing, @Nullable Locale locale) { - final Set firmwareSet = new HashSet<>(); - - ZigBeeFirmwareVersion requestedVersion = getRequestedVersionFromThing(thing); + ZigBeeFirmwareVersion requestedVersion = getThingRequestedVersion(thing); if (requestedVersion == null) { - return firmwareSet; + return Collections.emptySet(); } - for (DirectoryFileEntry firmware : directoryReader.getDirectory()) { - if (firmware.getFirmwareVersion().equals(requestedVersion)) { - firmwareSet.add(getZigBeeFirmware(thing.getThingTypeUID(), firmware)); - } + final Set directorySet = new HashSet<>(); + for (GithubLibraryReader reader : directoryReaders) { + directorySet.addAll(reader.getDirectorEntries(requestedVersion)); } + + final Set firmwareSet = new HashSet<>(); + for (DirectoryFileEntry firmware : directorySet) { + firmwareSet.add(getZigBeeFirmware(thing.getThingTypeUID(), firmware)); + } + + logger.debug("ZigBee Firmware Provider: Thing {} has {} firmwares available", thing.getUID(), + firmwareSet.size()); return firmwareSet; } - private ZigBeeFirmwareVersion getRequestedVersionFromThing(@NonNull Thing thing) { + private ZigBeeFirmwareVersion getThingRequestedVersion(@NonNull Thing thing) { // We only deal in ZigBee devices here if (!(thing.getHandler() instanceof ZigBeeThingHandler)) { return null; } + logger.debug("ZigBee Firmware Provider: Getting requested version of {}", thing.getUID()); ZigBeeThingHandler zigbeeHandler = (ZigBeeThingHandler) thing.getHandler(); if (zigbeeHandler == null) { + logger.info("ZigBee Firmware Provider: No handler found for {}", thing.getUID()); return null; } - return zigbeeHandler.getRequestedFirmwareVersion(); + ZigBeeFirmwareVersion version = zigbeeHandler.getRequestedFirmwareVersion(); + logger.debug("ZigBee Firmware Provider: Requested version of {} is {}", thing.getUID(), version); + return version; } private Firmware getZigBeeFirmware(@NonNull ThingTypeUID thingTypeUID, DirectoryFileEntry directoryEntry) { + return getZigBeeFirmware(thingTypeUID, directoryEntry, null); + } + + private Firmware getZigBeeFirmware(@NonNull ThingTypeUID thingTypeUID, DirectoryFileEntry directoryEntry, + InputStream inputStream) { FirmwareBuilder builder = FirmwareBuilder.create(thingTypeUID, directoryEntry.getVersion().toString()); if (!directoryEntry.getModel().isEmpty()) { @@ -138,6 +184,9 @@ private Firmware getZigBeeFirmware(@NonNull ThingTypeUID thingTypeUID, Directory if (!directoryEntry.getDescription().isEmpty()) { builder.withDescription(directoryEntry.getDescription()); } + if (!directoryEntry.getReleaseNotes().isEmpty()) { + builder.withChangelog(directoryEntry.getReleaseNotes()); + } if (!directoryEntry.getMd5().isEmpty()) { builder.withMd5Hash(directoryEntry.getMd5()); } @@ -145,6 +194,21 @@ private Firmware getZigBeeFirmware(@NonNull ThingTypeUID thingTypeUID, Directory builder.withPrerequisiteVersion(directoryEntry.getPrerequisiteVersion()); } + if (inputStream != null) { + builder.withInputStream(inputStream); + } + + Map properties = new HashMap<>(); + + if (directoryEntry.getFilesize() != null) { + properties.put("Filesize", directoryEntry.getFilesize().toString()); + } + if (directoryEntry.getFilename() != null) { + properties.put("Filename", directoryEntry.getFilename()); + } + + builder.withProperties(properties); + return builder.build(); } diff --git a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryEntry.java b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryEntry.java new file mode 100644 index 000000000..a06aca258 --- /dev/null +++ b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryEntry.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.zigbee.firmware.internal; + +/** + * Contains the information describing a firmware directory entry + * + * @author Chris Jackson + * + */ +public class DirectoryEntry { + +} diff --git a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFileEntry.java b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFileEntry.java index 289d2065a..c7ae29556 100644 --- a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFileEntry.java +++ b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFileEntry.java @@ -22,7 +22,8 @@ * @author Chris Jackson * */ -public class DirectoryFileEntry { +public class DirectoryFileEntry extends DirectoryEntry { + private String fileName; private Integer manufacturerCode; private Integer imageType; private Integer fileVersion; @@ -32,6 +33,17 @@ public class DirectoryFileEntry { private String modelId; private String url; + private String otaHeaderString; + + private Boolean force; + private Integer hardwareVersionMin; + private Integer hardwareVersionMax; + private Integer minFileVersion; + private Integer maxFileVersion; + private String originalUrl; + private String releaseNotes; + + // OH internal private String thingTypeUid; private String prerequisiteVersion; private String model; @@ -39,10 +51,17 @@ public class DirectoryFileEntry { private String description; private String md5; + /** + * @return the filename + */ + public String getFilename() { + return fileName; + } + /** * @return the fileSize */ - public Integer getFileSize() { + public Integer getFilesize() { return fileSize; } @@ -74,20 +93,6 @@ public String getSha512() { return sha512; } - /** - * @return the filesize - */ - public Integer getFilesize() { - return fileSize; - } - - /** - * @param filesize the filesize to set - */ - public void setFilesize(Integer filesize) { - this.fileSize = filesize; - } - /** * @return the thingTypeUid */ @@ -120,7 +125,13 @@ public String getVendor() { * @return the description */ public String getDescription() { - return description == null ? "" : description; + if (description != null) { + return description; + } + if (otaHeaderString != null) { + return otaHeaderString; + } + return null; } /** @@ -156,38 +167,72 @@ public List getManufacturerName() { } /** - * @param manufacturerName the manufacturerName to set + * @return the modelId */ - public void setManufacturerName(List manufacturerName) { - this.manufacturerName = manufacturerName; + public String getModelId() { + return modelId == null ? "" : modelId; } /** - * @return the modelId + * @return the url */ - public String getModelId() { - return modelId; + public String getUrl() { + return url; } /** - * @param modelId the modelId to set + * @return the otaHeaderString */ - public void setModelId(String modelId) { - this.modelId = modelId; + public String getOtaHeaderString() { + return otaHeaderString; } /** - * @return the url + * @return the force */ - public String getUrl() { - return url; + public Boolean getForce() { + return force == null ? Boolean.FALSE : force; + } + + /** + * @return the hardwareVersionMin + */ + public Integer getHardwareVersionMin() { + return hardwareVersionMin; + } + + /** + * @return the hardwareVersionMax + */ + public Integer getHardwareVersionMax() { + return hardwareVersionMax; } /** - * @param url the url to set + * @return the minFileVersion */ - public void setUrl(String url) { - this.url = url; + public Integer getMinFileVersion() { + return minFileVersion; } + /** + * @return the maxFileVersion + */ + public Integer getMaxFileVersion() { + return maxFileVersion; + } + + /** + * @return the originalUrl + */ + public String getOriginalUrl() { + return originalUrl; + } + + /** + * @return the releaseNotes + */ + public String getReleaseNotes() { + return releaseNotes == null ? "" : releaseNotes; + } } diff --git a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFolderEntry.java b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFolderEntry.java new file mode 100644 index 000000000..b60717aff --- /dev/null +++ b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/DirectoryFolderEntry.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.zigbee.firmware.internal; + +/** + * Contains the information describing a firmware directory entry + * + * @author Chris Jackson + * + */ +public class DirectoryFolderEntry extends DirectoryEntry { + private String name; + + DirectoryFolderEntry(String name) { + this.name = name; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + +} diff --git a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/GithubLibraryReader.java b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/GithubLibraryReader.java index a4246b269..32f1f977a 100644 --- a/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/GithubLibraryReader.java +++ b/org.openhab.binding.zigbee.firmware/src/main/java/org/openhab/binding/zigbee/firmware/internal/GithubLibraryReader.java @@ -26,19 +26,26 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory.Client; import org.openhab.binding.zigbee.ZigBeeBindingConstants; +import org.openhab.binding.zigbee.handler.ZigBeeFirmwareVersion; import org.openhab.core.OpenHAB; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,12 +63,18 @@ public class GithubLibraryReader { private final Logger logger = LoggerFactory.getLogger(GithubLibraryReader.class); private static final int HTTP_TIMEOUT = 5; - private static final String INDEX_JSON = "index.json"; + private static final int QUEUE_SIZE = 15; + private static final int UPDATE_CHECK_PERIOD = 40000; // Approximately twice per day - unsynchronisedd private static final String PATH_TO_FIRMWARE = "firmware"; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private ExecutorService executor = Executors.newCachedThreadPool(); + private BlockingQueue<@Nullable DirectoryEntry> commandQueue = new ArrayBlockingQueue<>(QUEUE_SIZE); + private @Nullable CommandProcessThread commandThread; + private boolean closeHandler = false; + private @Nullable ScheduledFuture updateJob = null; + private final Gson gson = new Gson(); private HttpClient httpClient; @@ -72,24 +85,20 @@ public class GithubLibraryReader { public GithubLibraryReader(String folder) { } - public List getDirectory() { - synchronized (directory) { - return directory; + public void close() { + closeHandler = true; + try { + commandThread.join(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + // e.printStackTrace(); } + stopUpdateJob(); } public boolean create(String repositoryAddress) throws Exception { logger.debug("ZigBee Firmware Provider: Creating directory at {}", repositoryAddress); - String localAddress; - if (repositoryAddress.endsWith("/")) { - localAddress = repositoryAddress.substring(0, repositoryAddress.length() - 1); - } else { - localAddress = repositoryAddress; - } - if (localAddress.contains("//")) { - localAddress = localAddress.substring(localAddress.indexOf("//") + 2); - } - this.repositoryAddress = localAddress + "/"; + this.repositoryAddress = repositoryAddress; Client sslContext = new SslContextFactory.Client(); this.httpClient = new HttpClient(sslContext); @@ -106,57 +115,42 @@ public boolean create(String repositoryAddress) throws Exception { return false; } - logger.debug("ZigBee Firmware Provider communicator created for {}", this.repositoryAddress); + logger.debug("ZigBee Firmware Provider: communicator created for {}", this.repositoryAddress); File folder = new File(OpenHAB.getUserDataFolder() + File.separator + ZigBeeBindingConstants.BINDING_ID + File.separator + PATH_TO_FIRMWARE); if (!folder.exists()) { - logger.debug("ZigBee Firmware Provider creating firmware folder {}", folder); + logger.debug("ZigBee Firmware Provider: creating firmware folder {}", folder); if (!folder.mkdirs()) { - logger.error("ZigBee Firmware Provider error creating firmware folder {}", folder); + logger.error("ZigBee Firmware Provider: error creating firmware folder {}", folder); } } - // Check if the index is available locally and load it - loadLocalDirectory(); + commandThread = new CommandProcessThread(); + commandThread.start(); + + startUpdateJob(); // We're done! return true; } - public void loadLocalDirectory() { - - // processDirectory(newDirectory); - } - - public void updateRemoteDirectory() { - logger.debug("ZigBee Firmware Provider: Scheduling update from remote"); - - Runnable commandHandler = new Runnable() { - @Override - public void run() { - logger.debug("ZigBee Firmware Provider: Starting update from remote"); - List newDirectory = getIndex(); - - processDirectory(newDirectory); - - createLocal(directory.get(0)); - } - }; - scheduler.execute(commandHandler); + public String getRepositoryAddress() { + return repositoryAddress; } private void processDirectory(List newDirectory) { + logger.debug("ZigBee Firmware Provider: Processing directory with {} entries", newDirectory.size()); for (DirectoryFileEntry entry : newDirectory) { File localFile = getLocalFile(entry); if (localFile.exists()) { - logger.debug("ZigBee Firmware Provider found local file '{}'", localFile); + logger.debug("ZigBee Firmware Provider: Found local file '{}'", localFile); // Check hash and delete local file if invalid InputStream stream = getInputStream(entry); if (stream == null) { - logger.debug("ZigBee Firmware Provider local file '{}' failed hash check and is deleted", + logger.debug("ZigBee Firmware Provider: Local file '{}' failed hash check and is deleted", localFile); localFile.delete(); } else { @@ -165,7 +159,7 @@ private void processDirectory(List newDirectory) { stream.close(); createMd5Hash(data, entry); } catch (IOException | NoSuchAlgorithmException e) { - logger.debug("ZigBee Firmware Provider local file '{}' failed hash check and is deleted: {}", + logger.debug("ZigBee Firmware Provider: Local file '{}' failed hash check and is deleted: {}", localFile, e.getLocalizedMessage()); localFile.delete(); } @@ -173,45 +167,38 @@ private void processDirectory(List newDirectory) { } } - if (newDirectory == null) { - logger.debug("ZigBee Firmware Provider directory update from GitHub failed!"); - return; - } - synchronized (directory) { directory.clear(); directory.addAll(newDirectory); + logger.debug("ZigBee Firmware Provider: Directory update completed - {} entries", newDirectory.size()); } } - public boolean isAvailableLocally(DirectoryFileEntry entry) { - return entry.getMd5() != null; - } - - public boolean createLocal(DirectoryFileEntry entry) { + private boolean createLocal(DirectoryFileEntry entry) { // Download from remote String url = entry.getUrl(); - logger.debug("ZigBee Firmware Provider: Requesting GitHub request: {}", url); + logger.debug("ZigBee Firmware Provider: Requesting GitHub file: {}", url); ContentResponse response; try { response = httpClient.newRequest(url).method(GET).timeout(HTTP_TIMEOUT, TimeUnit.SECONDS).send(); if (response.getStatus() != HttpURLConnection.HTTP_OK) { - logger.warn("ZigBee Firmware Provider return status other than HTTP_OK : {}", response.getStatus()); + logger.warn("ZigBee Firmware Provider: Return status other than HTTP_OK : {}", response.getStatus()); return false; } } catch (TimeoutException | ExecutionException | NullPointerException e) { - logger.warn("ZigBee Firmware Provider could not connect to server with exception: ", e); + logger.warn("ZigBee Firmware Provider: could not connect to server with exception: ", e); return false; } catch (InterruptedException e) { - logger.warn("ZigBee Firmware Provider connect to server interrupted: ", e); + logger.warn("ZigBee Firmware Provider: connect to server interrupted: ", e); Thread.currentThread().interrupt(); return false; } byte[] data = response.getContent(); + logger.debug("ZigBee Firmware Provider: GitHub downloaded {} bytes", data.length); // Check the hash if (checkHash(data, entry) == false) { @@ -222,7 +209,7 @@ public boolean createLocal(DirectoryFileEntry entry) { try { createMd5Hash(data, entry); } catch (NoSuchAlgorithmException e1) { - logger.error("System does not support MD5"); + logger.error("ZigBee Firmware Provider: System does not support MD5"); return false; } @@ -236,14 +223,19 @@ public boolean createLocal(DirectoryFileEntry entry) { outputStream.write(data, 0, data.length); outputStream.close(); } catch (FileNotFoundException e) { - logger.error("Can't find file {}", local.getName()); + logger.error("ZigBee Firmware Provider: Can't find file {}", local.getName()); } catch (IOException e) { - logger.error("IO Exception writing file {}", local.getName(), e); + logger.error("ZigBee Firmware Provider: IO Exception writing file {}", local.getName(), e); } + logger.debug("ZigBee Firmware Provider: GitHub file downloaded {}", url); return true; } + private void purgeOldFiles() { + + } + public InputStream getInputStream(DirectoryFileEntry entry) { File local = getLocalFile(entry); @@ -252,37 +244,37 @@ public InputStream getInputStream(DirectoryFileEntry entry) { try { inputStream = new FileInputStream(local); data = inputStream.readAllBytes(); + inputStream.close(); // Check the hash if (!checkHash(data, entry)) { - inputStream.close(); return null; } - return inputStream; + + return new FileInputStream(local); } catch (IOException e) { - logger.error("IO Exception reading file {}", local.getName(), e); + logger.error("ZigBee Firmware Provider: IO Exception reading file {}", local.getName(), e); return null; } } private synchronized List getIndex() { - String url = "https://" + repositoryAddress + INDEX_JSON; - - logger.debug("ZigBee Firmware Provider: Performing GitHub request: {}", url); + logger.debug("ZigBee Firmware Provider: Performing GitHub request: {}", repositoryAddress); ContentResponse response; try { - response = httpClient.newRequest(url).method(GET).timeout(HTTP_TIMEOUT, TimeUnit.SECONDS).send(); + response = httpClient.newRequest(repositoryAddress).method(GET).timeout(HTTP_TIMEOUT, TimeUnit.SECONDS) + .send(); if (response.getStatus() != HttpURLConnection.HTTP_OK) { - logger.warn("ZigBee Firmware Provider server return status other than HTTP_OK : {}", + logger.warn("ZigBee Firmware Provider: Server return status other than HTTP_OK : {}", response.getStatus()); return null; } } catch (TimeoutException | ExecutionException | NullPointerException e) { - logger.warn("ZigBee Firmware Provider could not connect to server with exception: ", e); + logger.warn("ZigBee Firmware Provider: Could not connect to server with exception: ", e); return null; } catch (InterruptedException e) { - logger.warn("ZigBee Firmware Provider connection to server interrupted: ", e); + logger.warn("ZigBee Firmware Provider: Connection to server interrupted: ", e); Thread.currentThread().interrupt(); return null; } @@ -303,13 +295,13 @@ private boolean checkHash(byte[] data, DirectoryFileEntry entry) { String hash = hashToString(messageDigest); if (!entry.getSha512().equalsIgnoreCase(hash)) { - logger.warn("ZigBee Firmware Provider SHA512 hash check on file {} failed", entry.getUrl()); + logger.warn("ZigBee Firmware Provider: SHA512 hash check on file {} failed", entry.getUrl()); return false; } return true; } catch (NoSuchAlgorithmException e) { - logger.warn("ZigBee Firmware Provider error checking hash on file {}: ", entry.getUrl(), e); + logger.warn("ZigBee Firmware Provider: Error checking hash on file {}: ", entry.getUrl(), e); } return false; @@ -338,10 +330,117 @@ private String hashToString(byte[] hash) { StringBuilder builder = new StringBuilder(); for (byte val : hash) { - builder.append(String.format("%02X", val)); + builder.append(String.format("%02x", val)); } return builder.toString(); } + private synchronized void downloadFile(DirectoryFileEntry entry) { + logger.debug("ZigBee Firmware Provider: Scheduling file download [{}]", entry.getUrl()); + + if (entry.getMd5() != "") { + logger.debug("ZigBee Firmware Provider: File [{}] already exists", entry.getUrl()); + return; + } + commandQueue.add(entry); + } + + private synchronized void updateDirectory() { + logger.debug("ZigBee Firmware Provider: Scheduling directory update"); + + commandQueue.add(new DirectoryFolderEntry(repositoryAddress)); + } + + private void startUpdateJob() { + stopUpdateJob(); + logger.debug("ZigBee Firmware Provider: Starting Update Job"); + this.updateJob = scheduler.scheduleWithFixedDelay(this::updateDirectory, 10, UPDATE_CHECK_PERIOD, + TimeUnit.SECONDS); + } + + private void stopUpdateJob() { + final ScheduledFuture updateJob = this.updateJob; + if (updateJob != null && !updateJob.isDone()) { + logger.debug("ZigBee Firmware Provider: Stopping Update Job"); + updateJob.cancel(false); + } + + this.updateJob = null; + } + + private class CommandProcessThread extends Thread { + CommandProcessThread() { + super("CommandProcessThread"); + } + + @Override + public void run() { + DirectoryEntry command; + + while (!interrupted()) { + if (closeHandler) { + break; + } + try { + command = commandQueue.take(); + logger.debug("ZigBee Firmware Provider: Took command from queue. Queue length={}, Command={}", + commandQueue.size(), command); + + if (command instanceof DirectoryFolderEntry) { + logger.debug("ZigBee Firmware Provider: Starting update from remote {}", repositoryAddress); + List newDirectory = getIndex(); + processDirectory(newDirectory); + purgeOldFiles(); + } + + if (command instanceof DirectoryFileEntry) { + DirectoryFileEntry fileEntry = (DirectoryFileEntry) command; + logger.debug("ZigBee Firmware Provider: Requesting remote file from {}", fileEntry.getUrl()); + createLocal(fileEntry); + } + } catch (final InterruptedException e) { + logger.debug("ZigBee Firmware Provider: Queue handler InterruptedException"); + break; + } catch (final Exception e) { + logger.error("ZigBee Firmware Provider: Queue handler exception", e); + } + } + } + } + + public Set getDirectorEntries(ZigBeeFirmwareVersion requestedVersion) { + final Set firmwareSet = new HashSet<>(); + synchronized (directory) { + for (DirectoryFileEntry firmware : directory) { + if (firmware.getManufacturerCode().equals(requestedVersion.getManufacturerCode()) + && firmware.getImageType().equals(requestedVersion.getFileType())) { + firmwareSet.add(firmware); + } + } + } + + for (DirectoryFileEntry firmware : firmwareSet) { + if (firmware.getMd5() != null) { + downloadFile(firmware); + } + } + return firmwareSet; + } + + public DirectoryFileEntry getDirectoryEntry(ZigBeeFirmwareVersion requestedVersion, int specificVersion) { + synchronized (directory) { + for (DirectoryFileEntry firmware : directory) { + if (firmware.getManufacturerCode().equals(requestedVersion.getManufacturerCode()) + && firmware.getImageType().equals(requestedVersion.getFileType()) + && firmware.getVersion().equals(specificVersion)) { + logger.debug("ZigBee Firmware Provider: Found firmware version {}", specificVersion); + return firmware; + } + } + } + + logger.debug("ZigBee Firmware Provider: Unable to find firmware version {}", specificVersion); + return null; + } } diff --git a/org.openhab.binding.zigbee/README.md b/org.openhab.binding.zigbee/README.md index 8b7736ca8..49553063d 100644 --- a/org.openhab.binding.zigbee/README.md +++ b/org.openhab.binding.zigbee/README.md @@ -499,18 +499,22 @@ ZigBee has a standard way of configuring how a device sends status reports to th - Maximum Reporting Period: This is the maximum time between reports that the device will send updates. If the data never changes, then the device will still send an update at this rate. This is important so that the binding knows the device has not failed, so it should not be set too long (normally a couple of hours will be fine). - Change: This is only applicable for "Analogue" data such as temperature, humidity, power. If the value changes by this amount since the last update, then an update will be sent so long as the minimum reporting period has passed. -In order for a report to be sent to the binding, or to another device, a "binding" must also be configured. Binding and Reporting work together - *binding* tells the device WHERE to send reports, while *reporting* tells the device WHAT to send. The binding will set up binding and reporting automatically to get the information that it requires to provide user feedback or update channels, however it may also be desirable to configure a device to automatically send a command to another device without going through openHAB. For example, a wall switch might be configured to directly turn a light On or Off without sending the command from the switch to the openHAB binding, and for the binding to send another command back to the light. Such configuration may be performed through the openHAB command line interface which is described below. +In order for a report to be sent to the binding, or to another device, a "binding" must also be configured. Binding and Reporting work together - _binding_ tells the device WHERE to send reports, while _reporting_ tells the device WHAT to send. The binding will set up binding and reporting automatically to get the information that it requires to provide user feedback or update channels, however it may also be desirable to configure a device to automatically send a command to another device without going through openHAB. For example, a wall switch might be configured to directly turn a light On or Off without sending the command from the switch to the openHAB binding, and for the binding to send another command back to the light. Such configuration may be performed through the openHAB command line interface which is described below. Polling may be used by the binding to request data from the device. Polling is normally only used if reporting doesn't work for some reason. This may happen if the reporting table in a device is full - if the binding detects this, it will increase the polling rate. ## Device Firmware Updates -A *Firmware Provider*, backed by the [Koenkk OTA](https://github.com/Koenkk/zigbee-OTA) repository on GitHub can be used to upgrade device firmware. This *Firmware Provider* provides firmware to the openHAB firmware management system. Since there is no information linking firmware to a device, Zigbee devices must ask for a firmware update, and when this happens, the *Firmware Provider* will use the information in this request to check to see if there is firmware available, and if there is it will download this to a local file in the *Userdata* folder. It will also advise the openHAB firmware management system that there is firmware available to upload, and the user can manage this appropriately. +A _Firmware Provider_, backed by the [Koenkk OTA](https://github.com/Koenkk/zigbee-OTA) repository on GitHub can be used to upgrade (or downgrade) device firmware. This _Firmware Provider_ provides firmware to the openHAB firmware management system. Since there is no information linking firmware to a device, Zigbee devices must request a firmware update, and when this happens, the _Firmware Provider_ will use the information in this request to check to see if there is firmware available. If there is it will download this to a local file in the _Userdata_ folder. It will also advise the openHAB firmware management system that there is firmware available to upload, and the user can manage this appropriately. The firmware update will commence once the binding receives the next request from the device following the user starting the firmware update. Note that the thing will be offline while performing the OTA update. Devices normally request a firmware update at an interval that could be every few minutes, to every few days - depending on the manufacturer. When the provider receives the request from the device, it checks to see if there is a firmware matching the request, and if so it will download the firmware from the net in preparation for the user to approve the upgrade. Firmware files downloaded from the repository are checked for integrity against the SHA512 hash. an MD5 hash is then generated locally so that the firmware can be checked by the OH core prior to starting the firmware update. +Firmware updates generate a lot of traffic on the network, and will take a reasonable amount of time to complete - tens of minutes or longer. It is best not to update multiple devices at the same time, and probably best to do the update at a quiet time as network latency may increase. + +Some devices may leave the network following an OTA upgrade. It's also worth noting that while rare, updating firmware has the potential to brick your device so it is recommended to upgrade only if the new firmware brings features you require. + Currently the openHAB main UI doesn't support the firmware management system, so this must be performed using the console. ## When things don't appear to be working @@ -547,7 +551,7 @@ The binding table is used within ZigBee to configure devices to send reports to The binding table for a node can be displayed with the `bindtable` command. It can then be updated with the `bind` command, and bindings can be removed with the `unbind` command. -A second part to the binding and reporting system is the reporting. The binding table tells the device where it should send reports, but the actual reports must be configured as well. Many attributes in a ZigBee cluster can be configured to send reports if their state changes, or at a periodical rate if there have been no state updates within a certain time. Analogue values can be configured so that they report if the value changes by a certain amount so that the reports do not flood the system. Care must be exercised when changing this configuration as it may interfere with the binding operation. +A second part to the binding and reporting system is the reporting. The binding table tells the device where it should send reports, but the actual reports must be configured as well. Many attributes in a ZigBee cluster can be configured to send reports if their state changes, or at a periodical rate if there have been no state updates within a certain time. Analogue values can be configured so that they report if the value changes by a certain amount so that the reports do not flood the system. Care must be exercised when changing this configuration as it may interfere with the binding operation. The exact command required to configure reporting can depend on whether the attribute is a binary or analogue type. The console commands `subscribe` and `unsubscribe` allow the user to manipulate the reporting of an attribute, and the `reportcfg` command can be used to display the current configuration. diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java index 1c84c0086..a32c76af1 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java @@ -311,7 +311,6 @@ public class ZigBeeBindingConstants { public static final String OFFLINE_DISCOVERY_INCOMPLETE = "@text/zigbee.status.offline_discoveryincomplete"; public static final String FIRMWARE_FAILED = "@text/zigbee.firmware.failed"; - public static final String FIRMWARE_VERSION_HEX_PREFIX = "0x"; // List of channel state constants public static final String STATE_OPTION_BATTERY_MIN_THRESHOLD = "minThreshold"; diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/discovery/ZigBeeNodePropertyDiscoverer.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/discovery/ZigBeeNodePropertyDiscoverer.java index bc6b33ce3..ed7fcd88b 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/discovery/ZigBeeNodePropertyDiscoverer.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/discovery/ZigBeeNodePropertyDiscoverer.java @@ -254,7 +254,7 @@ private void addPropertiesFromOtaCluster(ZigBeeNode node) { ZclAttribute attribute = otaCluster.getAttribute(ZclOtaUpgradeCluster.ATTR_CURRENTFILEVERSION); Object fileVersion = attribute.readValue(Long.MAX_VALUE); if (fileVersion != null) { - properties.put(PROPERTY_FIRMWARE_VERSION, String.format("0x%08X", fileVersion)); + properties.put(PROPERTY_FIRMWARE_VERSION, fileVersion.toString()); } else { logger.debug("{}: Could not get OTA firmware version from device", node.getIeeeAddress()); } diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeFirmwareVersion.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeFirmwareVersion.java index 24169ea71..4c074185d 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeFirmwareVersion.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeFirmwareVersion.java @@ -38,6 +38,12 @@ public ZigBeeFirmwareVersion(final int manufacturerCode, final int imageType, fi this.fileVersion = fileVersion; } + public ZigBeeFirmwareVersion(final int manufacturerCode, final int imageType) { + this.manufacturerCode = manufacturerCode; + this.imageType = imageType; + this.fileVersion = 0; + } + /** * @return the manufacturerCode */ diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeThingHandler.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeThingHandler.java index 85dfdf105..c6df2994e 100755 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeThingHandler.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/handler/ZigBeeThingHandler.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; @@ -83,6 +84,8 @@ import com.zsmartsystems.zigbee.app.otaserver.ZigBeeOtaFile; import com.zsmartsystems.zigbee.app.otaserver.ZigBeeOtaServerStatus; import com.zsmartsystems.zigbee.app.otaserver.ZigBeeOtaStatusCallback; +import com.zsmartsystems.zigbee.zcl.ZclAttribute; +import com.zsmartsystems.zigbee.zcl.ZclCluster; import com.zsmartsystems.zigbee.zcl.clusters.ZclOtaUpgradeCluster; import com.zsmartsystems.zigbee.zcl.clusters.otaupgrade.QueryNextImageCommand; import com.zsmartsystems.zigbee.zdo.field.NeighborTable; @@ -171,7 +174,7 @@ public class ZigBeeThingHandler extends BaseThingHandler implements ZigBeeNetwor /** * Holds the version information from the last request the device made */ - private ZigBeeFirmwareVersion lastFirmwareVersion; + private ZigBeeFirmwareVersion firmwareVersion; private boolean firmwareUpdateInProgress = false; @@ -469,7 +472,22 @@ private synchronized void doNodeInitialisation() { // Listen for incoming OTA requests ZclOtaUpgradeServer otaServer = getOtaServer(node); + logger.debug("{}: OTA Server = {}", nodeIeeeAddress, otaServer); if (otaServer != null) { + Optional cluster = node.getEndpoints().stream() + .map(ep -> ep.getOutputCluster(ZclOtaUpgradeCluster.CLUSTER_ID)).filter(Objects::nonNull) + .findFirst(); + ZclOtaUpgradeCluster otaCluster = (ZclOtaUpgradeCluster) cluster.orElse(null); + + if (otaCluster != null) { + ZclAttribute attribute; + + attribute = otaCluster.getAttribute(ZclOtaUpgradeCluster.ATTR_CURRENTFILEVERSION); + Object fileVersion = attribute.readValue(Long.MAX_VALUE); + if (fileVersion != null) { + updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, fileVersion.toString()); + } + } otaServer.addListener(this); } @@ -626,12 +644,12 @@ private void startPolling() { @Override public void run() { try { - logger.debug("{}: Polling {} channels", nodeIeeeAddress, channels.keySet().size()); + logger.debug("{}: Polling [{} channels]", nodeIeeeAddress, channels.keySet().size()); for (ChannelUID channelUid : channels.keySet()) { if (!isLinked(channelUid)) { // Don't poll if this channel isn't linked - logger.debug("{}: Not polling {} - channel is not linked", nodeIeeeAddress, channelUid); + logger.trace("{}: Not polling {} - channel is not linked", nodeIeeeAddress, channelUid); continue; } @@ -828,8 +846,8 @@ public void setChannelState(ChannelUID channel, State state) { @Override public void triggerChannel(ChannelUID channel, String event) { if (firmwareUpdateInProgress) { - logger.debug("Omitting triggering ZigBee channel {} with event {} due to firmware update in progress", - channel, event); + logger.debug("{}: Omitting triggering ZigBee channel {} with event {} due to firmware update in progress", + nodeIeeeAddress, channel, event); return; } logger.debug("{}: Triggering ZigBee channel {} with event {}", nodeIeeeAddress, channel, event); @@ -1009,20 +1027,24 @@ private ZclOtaUpgradeServer getOtaServer(ZigBeeNode node) { @Override public void otaStatusUpdate(ZigBeeOtaServerStatus status, int percent) { - logger.debug("{}: OTA transfer status update {}, percent={}", nodeIeeeAddress, status, percent); + logger.debug("{}: OTA transfer status update firmwareUpdateInProgress={}, status={}, percent={}", + nodeIeeeAddress, firmwareUpdateInProgress, status, percent); + if (progressCallback != null) { switch (status) { case OTA_WAITING: - // DOWNLOADING - progressCallback.next(); + progressCallback.next(); // WAITING + return; + case OTA_STARTED: + progressCallback.next(); // TRANSFERRING return; case OTA_TRANSFER_IN_PROGRESS: + isAliveTracker.resetTimer(this); progressCallback.update(percent); return; case OTA_TRANSFER_COMPLETE: - // REBOOTING - progressCallback.next(); progressCallback.update(100); + progressCallback.next(); // UPDATING return; case OTA_UPGRADE_COMPLETE: progressCallback.success(); @@ -1033,20 +1055,23 @@ public void otaStatusUpdate(ZigBeeOtaServerStatus status, int percent) { case OTA_CANCELLED: progressCallback.canceled(); break; - default: + case OTA_UNINITIALISED: + return; + case OTA_UPGRADE_FIRMWARE_RESTARTING: + progressCallback.next(); // REBOOTING + return; + case OTA_UPGRADE_WAITING: return; } } // OTA transfer is complete, cancelled or failed firmwareUpdateInProgress = false; - otaServer.cancelUpgrade(); for (int retry = 0; retry < 3; retry++) { Integer fileVersion = otaServer.getCurrentFileVersion(); if (fileVersion != null) { - updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, - String.format("%s%08X", ZigBeeBindingConstants.FIRMWARE_VERSION_HEX_PREFIX, fileVersion)); + updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, fileVersion.toString()); break; } else { logger.debug("{}: OTA firmware request timeout (retry {})", nodeIeeeAddress, retry); @@ -1054,14 +1079,17 @@ public void otaStatusUpdate(ZigBeeOtaServerStatus status, int percent) { } updateStatus(ThingStatus.ONLINE); + startPolling(); progressCallback = null; } @Override public ZigBeeOtaFile otaIncomingRequest(QueryNextImageCommand command) { // We simply store the requested firmware version information so that it's available for the firmware provider - lastFirmwareVersion = new ZigBeeFirmwareVersion(command.getManufacturerCode(), command.getImageType(), + firmwareVersion = new ZigBeeFirmwareVersion(command.getManufacturerCode(), command.getImageType(), command.getFileVersion()); + logger.debug("{}: OTA firmware request received {}", nodeIeeeAddress, command); + updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, command.getFileVersion().toString()); // We always return null as we don't want to automatically start the OTA // Instead we should use the OH concept for firmware management which let's the user know there's a @@ -1075,13 +1103,14 @@ public ZigBeeOtaFile otaIncomingRequest(QueryNextImageCommand command) { * @return the {@link ZigBeeFirmwareVersion} or null if no request has been received from the device */ public ZigBeeFirmwareVersion getRequestedFirmwareVersion() { - return lastFirmwareVersion; + return firmwareVersion; } @Override public void updateFirmware(Firmware firmware, ProgressCallback progressCallback) { if (nodeIeeeAddress == null) { logger.debug("Unable to update firmware as node address is unknown", nodeIeeeAddress); + progressCallback.failed("zigbee.firmware.failed_unknown"); return; } logger.debug("{}: Update firmware with {}", nodeIeeeAddress, firmware.getVersion()); @@ -1089,26 +1118,27 @@ public void updateFirmware(Firmware firmware, ProgressCallback progressCallback) // Find an OTA client if the device supports OTA upgrades ZigBeeNode node = coordinatorHandler.getNode(nodeIeeeAddress); if (node == null) { - logger.debug("{}: Can't find node", nodeIeeeAddress); + logger.debug("{}: Update firmware failed - can't find node", nodeIeeeAddress); + progressCallback.failed("Node {} not found!", nodeIeeeAddress); return; } - ZclOtaUpgradeServer otaServer = getOtaServer(node); + this.progressCallback = progressCallback; + otaServer = getOtaServer(node); // Set ourselves offline, and prevent going back online firmwareUpdateInProgress = true; + stopPolling(); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.FIRMWARE_UPDATING); // Define the sequence of the firmware update so that external consumers can listen for the progress - progressCallback.defineSequence(ProgressStep.TRANSFERRING, ProgressStep.REBOOTING); + progressCallback.defineSequence(ProgressStep.WAITING, ProgressStep.TRANSFERRING, ProgressStep.UPDATING, + ProgressStep.REBOOTING); ZigBeeOtaFile otaFile = new ZigBeeOtaFile(firmware.getBytes()); otaServer.setFirmware(otaFile); - // DOWNLOADING - progressCallback.next(); - - this.progressCallback = progressCallback; + logger.debug("{}: Update firmware OK!!", nodeIeeeAddress); } @Override diff --git a/org.openhab.binding.zigbee/src/main/resources/OH-INF/i18n/thingstate.properties b/org.openhab.binding.zigbee/src/main/resources/OH-INF/i18n/thingstate.properties index 7b3bf5c45..41c4fbdde 100644 --- a/org.openhab.binding.zigbee/src/main/resources/OH-INF/i18n/thingstate.properties +++ b/org.openhab.binding.zigbee/src/main/resources/OH-INF/i18n/thingstate.properties @@ -8,3 +8,4 @@ zigbee.status.offline_nodenotfound=Node is not found on network zigbee.status.offline_discoveryincomplete=Node has not completed discovery zigbee.firmware.failed=Firmware update failed +zigbee.firmware.failed_unknown=Firmware update failed - node unknown