Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ to `$OH_CONFDIR/services/runtime.cfg` .
| `logPrefix` | `text` | Log prefix to use for this device. | deviceId | no | yes |
| `deviceLogLevel` | `text` | ESPHome device log level to stream from the device. | NONE | no | yes |
| `enableBluetoothProxy` | `boolean` | Allow this device to proxy Bluetooth traffic. Requires ESPHome device to be configured with `bluetooth_proxy` | false | no | yes |
| `configFileFullPath` | `string` | Fully qualified path to esphome yaml for this device. Used for firmware upgrades. | false | no | yes |

## Channels

Expand All @@ -90,15 +91,21 @@ be linked to an Item. Instead, they fire an event on the openHAB event bus when
You can use these in rules like this:

```js
configuration: {}
configuration: {
}
triggers:
- id: "1"
label: My Event Channel triggered
pluginId: core.ChannelEventTrigger
type: core.ChannelEventTrigger
configuration:
event: dag
channelUID: esphome:device:mydevice:scene_dag
-id
:
"1"
label: My
Event
Channel
triggered
pluginId: core.ChannelEventTrigger
type: core.ChannelEventTrigger
configuration:
event: dag
channelUID: esphome:device:mydevice:scene_dag
```

Or in Rules DSL:
Expand Down Expand Up @@ -386,6 +393,32 @@ For JRuby, use the [
For Python Scripting, use [
`GenericEventTrigger`](https://www.openhab.org/addons/automation/pythonscripting/#module-openhab-triggers).

## Firmware upgrade support

The binding can now trigger firmware updates via a `Thing` action.

It finds the latest firmware version by checking https://github.com/esphome/esphome/releases/latest at startup
and every 24 hours.

2 channels are automatically added to each thing:

- `latestFirmwareVersion` (`String`) which contains the latest firmware version.
- `firmwareUpdateAvailable` (`Contact`) where `OPEN` indicates a newer esphome version is available

The following configuration must be in place for firmware **upgrade** to work:

1. Set the `Thing` configuration parameter `configFileFullPath` to the fully qualified path of the esphome yaml file.
2. In the addon/binding configuration, add the fully qualified path to `esphome` binary. Parameter is
`esphomeExecutable`
if not done via MainUI
3. Similarly, add the command to run to upgrade your local installation of esphome to the latest version. For Homebrew (
MacOS) this could be `brew upgrade esphome`. Parameter is `esphomeUpgradeExecutable` if not done via MainUI.

> Note: Point 2 and 3 really depend on what OS your machine is running and whether Docker is involved or not. If you get
> it working, feel free to add an example section below for your OS and Docker setup.

> Note2: The upgrade process runs even if the device is running the latest esphome version available.

## Limitations

Most entity types and functions are now supported. However, there are some limitations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ public class ESPHomeConfiguration {
public LogLevel deviceLogLevel = LogLevel.NONE;

public boolean enableBluetoothProxy = false;

public String configFileFullPath;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package no.seime.openhab.binding.esphome.internal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FirmwareUpgradeService {

private final Logger logger = LoggerFactory.getLogger(FirmwareUpgradeService.class);

private String bindingPropertyEspHomeExecutable;
private String bindingPropertyEspHomeUpgradeExecutable;

public @Nullable String getInstalledVersion() {
if (bindingPropertyEspHomeExecutable == null) {
return null;
}

try {
Process process = new ProcessBuilder(bindingPropertyEspHomeExecutable, "version").redirectErrorStream(true)
.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("Version: ")) {
return line.substring("Version: ".length()).trim();
}
}
}
process.waitFor();
} catch (IOException e) {
logger.warn("Failed to run esphome version command", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}

public synchronized boolean upgradeLocalInstallation() {
if (bindingPropertyEspHomeUpgradeExecutable == null) {
logger.warn("No esphome upgrade executable configured");
return false;
}
try {
Process process = new ProcessBuilder(bindingPropertyEspHomeUpgradeExecutable).redirectErrorStream(true)
.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
logger.debug("esphome upgrade: {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode == 0) {
return true;
} else {
logger.warn("esphome upgrade command exited with code {}", exitCode);
return false;
}
} catch (IOException e) {
logger.warn("Failed to run esphome upgrade command", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}

public boolean upgradeDevice(String configFilePath) {
if (bindingPropertyEspHomeExecutable == null) {
logger.warn("No esphome executable configured");
return false;
}
try {
Process process = new ProcessBuilder(bindingPropertyEspHomeExecutable, "run", configFilePath, "--no-logs")
.redirectErrorStream(true).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
logger.debug("esphome run: {}", line);
}
}
boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.MINUTES);
if (!finished) {
process.destroyForcibly();
logger.warn("esphome run command timed out after 5 minutes for config: {}", configFilePath);
return false;
}
int exitCode = process.exitValue();
if (exitCode == 0) {
return true;
} else {
logger.warn("esphome run command exited with code {} for config: {}", exitCode, configFilePath);
return false;
}
} catch (IOException e) {
logger.warn("Failed to run esphome run command for config: {}", configFilePath, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}

public void setBindingPropertyEspHomeExecutable(@Nullable String bindingPropertyEspHomeExecutable) {
this.bindingPropertyEspHomeExecutable = bindingPropertyEspHomeExecutable;
}

public void setBindingPropertyEspHomeUpgradeExecutable(@Nullable String bindingPropertyEspHomeUpgradeExecutable) {
this.bindingPropertyEspHomeUpgradeExecutable = bindingPropertyEspHomeUpgradeExecutable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
import io.esphome.api.*;
import no.seime.openhab.binding.esphome.events.ESPHomeEventFactory;
import no.seime.openhab.binding.esphome.internal.*;
import no.seime.openhab.binding.esphome.internal.LogLevel;
import no.seime.openhab.binding.esphome.internal.bluetooth.ESPHomeBluetoothProxyHandler;
import no.seime.openhab.binding.esphome.internal.comm.*;
import no.seime.openhab.binding.esphome.internal.handler.action.AbstractESPHomeThingAction;
import no.seime.openhab.binding.esphome.internal.handler.action.DynamicThingActionsGenerator;
import no.seime.openhab.binding.esphome.internal.handler.action.FirmwareUpgradeAction;
import no.seime.openhab.binding.esphome.internal.message.*;
import no.seime.openhab.binding.esphome.internal.message.statesubscription.ESPHomeEventSubscriber;
import no.seime.openhab.binding.esphome.internal.message.statesubscription.EventSubscription;
Expand Down Expand Up @@ -85,9 +85,10 @@ public class ESPHomeHandler extends BaseThingHandler implements CommunicationLis
private final KeySequentialExecutor packetProcessor;
private final EventPublisher eventPublisher;
@Nullable
private final String defaultEncryptionKey;
private final String bindingPropertyDefaultEncryptionKey;
private final BundleContext bundleContext;
private final ESPHomeVersionService versionService;
private final FirmwareUpgradeService firmwareUpgradeService;
private @Nullable ESPHomeConfiguration config;
private @Nullable EncryptedFrameHelper frameHelper;
@Nullable
Expand Down Expand Up @@ -119,8 +120,9 @@ public class ESPHomeHandler extends BaseThingHandler implements CommunicationLis
public ESPHomeHandler(Thing thing, ConnectionSelector connectionSelector,
ESPChannelTypeProvider dynamicChannelTypeProvider, ESPStateDescriptionProvider stateDescriptionProvider,
ESPHomeEventSubscriber eventSubscriber, MonitoredScheduledThreadPoolExecutor executorService,
KeySequentialExecutor packetProcessor, EventPublisher eventPublisher, @Nullable String defaultEncryptionKey,
BundleContext bundleContext, ESPHomeVersionService versionService) {
KeySequentialExecutor packetProcessor, EventPublisher eventPublisher,
@Nullable String bindingPropertyDefaultEncryptionKey, BundleContext bundleContext,
ESPHomeVersionService versionService, FirmwareUpgradeService firmwareUpgradeService) {
super(thing);
this.connectionSelector = connectionSelector;
this.dynamicChannelTypeProvider = dynamicChannelTypeProvider;
Expand All @@ -130,9 +132,10 @@ public ESPHomeHandler(Thing thing, ConnectionSelector connectionSelector,
this.executorService = executorService;
this.packetProcessor = packetProcessor;
this.eventPublisher = eventPublisher;
this.defaultEncryptionKey = defaultEncryptionKey;
this.bindingPropertyDefaultEncryptionKey = bindingPropertyDefaultEncryptionKey;
this.bundleContext = bundleContext;
this.versionService = versionService;
this.firmwareUpgradeService = firmwareUpgradeService;

// Register message handlers for each type of message pairs
registerMessageHandler(EntityTypes.SELECT, new SelectMessageHandler(this), ListEntitiesSelectResponse.class,
Expand Down Expand Up @@ -274,8 +277,8 @@ private void connect() {
// Default to using the default encryption key from the binding if not set in device configuration
String encryptionKey = config.encryptionKey;
if (encryptionKey == null || encryptionKey.isEmpty()) {
if (defaultEncryptionKey != null) {
encryptionKey = defaultEncryptionKey;
if (bindingPropertyDefaultEncryptionKey != null) {
encryptionKey = bindingPropertyDefaultEncryptionKey;
logger.info("[{}] Using binding default encryption key", logPrefix);
} else {
logger.warn("[{}] No encryption key configured on neither binding nor thing. Cannot continue",
Expand Down Expand Up @@ -502,6 +505,9 @@ private void handleConnected(GeneratedMessage message) throws ProtocolAPIError {

addFirmwareChannels();

thingActionServiceRegistrations.add(bundleContext.registerService(ThingActions.class,
new FirmwareUpgradeAction(this), new Hashtable<>()));

updateThing(editThing().withChannels(dynamicChannels).build());
logger.debug("[{}] Device interrogation complete, done updating thing channels", logPrefix);
interrogated = true;
Expand Down Expand Up @@ -691,7 +697,7 @@ private void handleHelloResponse(GeneratedMessage message) throws ProtocolAPIErr
logger.debug("[{}] Requesting device to send actions and events", logPrefix);
frameHelper.send(SubscribeHomeassistantServicesRequest.getDefaultInstance());
}
if (config.deviceLogLevel != LogLevel.NONE) {
if (config.deviceLogLevel != no.seime.openhab.binding.esphome.internal.LogLevel.NONE) {
logger.info("[{}] Starting to stream logs to logger " + DEVICE_LOGGER_NAME, logPrefix);

frameHelper.send(SubscribeLogsRequest.newBuilder()
Expand Down Expand Up @@ -969,4 +975,12 @@ private enum ConnectionState {
CONNECTED

}

public FirmwareUpgradeService getFirmwareUpgradeService() {
return firmwareUpgradeService;
}

public ESPHomeVersionService getVersionService() {
return versionService;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

import no.seime.openhab.binding.esphome.internal.BindingConstants;
import no.seime.openhab.binding.esphome.internal.ESPHomeVersionService;
import no.seime.openhab.binding.esphome.internal.FirmwareUpgradeService;
import no.seime.openhab.binding.esphome.internal.bluetooth.ESPHomeBluetoothProxyHandler;
import no.seime.openhab.binding.esphome.internal.comm.ConnectionSelector;
import no.seime.openhab.binding.esphome.internal.message.statesubscription.ESPHomeEventSubscriber;
Expand All @@ -56,8 +57,9 @@ public class ESPHomeHandlerFactory extends BaseThingHandlerFactory {

private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(BindingConstants.THING_TYPE_DEVICE,
BindingConstants.THING_TYPE_BLE_PROXY);
public FirmwareUpgradeService firmwareUpgradeService;

private @Nullable String defaultEncryptionKey;
private @Nullable String bindingPropertyDefaultEncryptionKey;

private final AtomicLong threadCounter = new AtomicLong(0);

Expand Down Expand Up @@ -104,6 +106,8 @@ public ESPHomeHandlerFactory(@Reference ESPChannelTypeProvider dynamicChannelTyp
connectionSelector = new ConnectionSelector();

versionService = new ESPHomeVersionService(scheduler);

firmwareUpgradeService = new FirmwareUpgradeService();
}

@Override
Expand All @@ -113,7 +117,7 @@ public ESPHomeHandlerFactory(@Reference ESPChannelTypeProvider dynamicChannelTyp
if (BindingConstants.THING_TYPE_DEVICE.equals(thingTypeUID)) {
ESPHomeHandler handler = new ESPHomeHandler(thing, connectionSelector, dynamicChannelTypeProvider,
stateDescriptionProvider, eventSubscriber, scheduler, packetExecutor, eventPublisher,
defaultEncryptionKey, getBundleContext(), versionService);
bindingPropertyDefaultEncryptionKey, getBundleContext(), versionService, firmwareUpgradeService);
esphomeHandlers.put(thing.getUID(), handler);
return handler;
} else if (BindingConstants.THING_TYPE_BLE_PROXY.equals(thingTypeUID)) {
Expand All @@ -132,11 +136,18 @@ protected void activate(ComponentContext componentContext) {
connectionSelector.start();
versionService.start();
Dictionary<String, Object> properties = componentContext.getProperties();
defaultEncryptionKey = StringUtils.trimToNull((String) properties.get("defaultEncryptionKey"));
if (defaultEncryptionKey != null) {
bindingPropertyDefaultEncryptionKey = StringUtils.trimToNull((String) properties.get("defaultEncryptionKey"));
if (bindingPropertyDefaultEncryptionKey != null) {
logger.info(
"Found binding default encryption key for ESPHome devices, will use if not configured on thing");
}

String bindingPropertyEspHomeExecutable = StringUtils.trimToNull((String) properties.get("esphomeExecutable"));
String bindingPropertyEspHomeUpgradeExecutable = StringUtils
.trimToNull((String) properties.get("esphomeUpgradeExecutable"));

firmwareUpgradeService.setBindingPropertyEspHomeExecutable(bindingPropertyEspHomeExecutable);
firmwareUpgradeService.setBindingPropertyEspHomeUpgradeExecutable(bindingPropertyEspHomeUpgradeExecutable);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public abstract class AbstractESPHomeThingAction implements ThingActions {

@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof ESPHomeHandler bridgeHandler) {
this.handler = bridgeHandler;
if (handler instanceof ESPHomeHandler espHomeHandler) {
this.handler = espHomeHandler;
}
}

Expand Down
Loading
Loading