Skip to content

Commit

Permalink
Shelly 2.5: refactored to use HttpBridge (#2573)
Browse files Browse the repository at this point in the history
* Refactored to use HttpBridge
* Fix errors; improve code quality

---------

Co-authored-by: Stefan Feilmeier <[email protected]>
  • Loading branch information
Sn0w3y and sfeilmeier authored Jun 1, 2024
1 parent 68186f1 commit 6937f29
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 235 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
import io.openems.common.channel.AccessMode;
import io.openems.common.channel.Level;
import io.openems.common.channel.PersistencePriority;
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.types.OpenemsType;
import io.openems.edge.common.channel.BooleanDoc;
import io.openems.edge.common.channel.BooleanWriteChannel;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.channel.StateChannel;
import io.openems.edge.common.channel.value.Value;
Expand Down Expand Up @@ -41,6 +39,29 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId
.accessMode(AccessMode.READ_WRITE) //
.persistencePriority(PersistencePriority.HIGH) //
.onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_1)),

/**
* Indicates whether the associated Relay is in Overtemp-State.
*
* <ul>
* <li>Interface: Shelly25
* <li>Type: Boolean
* <li>Level: WARN
* </ul>
*/
RELAY_1_OVERTEMP(Doc.of(Level.WARNING) //
.text("Relay 1 has been switched off due to Overtemperature.")),
/**
* Indicates whether the associated Relay is in Overpower-State.
*
* <ul>
* <li>Interface: Shelly25
* <li>Type: Boolean
* <li>Level: WARN
* </ul>
*/
RELAY_1_OVERPOWER(Doc.of(Level.WARNING) //
.text("Relay 2 has been switched off due to Overpower.")),
/**
* Holds writes to Relay Output 2 for debugging.
*
Expand All @@ -64,6 +85,28 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId
.accessMode(AccessMode.READ_WRITE) //
.persistencePriority(PersistencePriority.HIGH) //
.onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_2)),
/**
* Indicates whether the associated Relay is in Overtemp-State.
*
* <ul>
* <li>Interface: Shelly25
* <li>Type: Boolean
* <li>Level: WARN
* </ul>
*/
RELAY_2_OVERTEMP(Doc.of(Level.WARNING) //
.text("Relay 2 has been switched off due to Overtemperature.")),
/**
* Indicates whether the associated Relay is in Overpower-State.
*
* <ul>
* <li>Interface: Shelly25
* <li>Type: Boolean
* <li>Level: WARN
* </ul>
*/
RELAY_2_OVERPOWER(Doc.of(Level.WARNING) //
.text("Relay 2 has been switched off due to Overpower.")),
/**
* Slave Communication Failed Fault.
*
Expand All @@ -86,80 +129,6 @@ public Doc doc() {
}
}

/**
* Gets the Channel for {@link ChannelId#RELAY_1}.
*
* @return the Channel
*/
public default BooleanWriteChannel getRelay1Channel() {
return this.channel(ChannelId.RELAY_1);
}

/**
* Gets the Relay Output 1. See {@link ChannelId#RELAY_1}.
*
* @return the Channel {@link Value}
*/
public default Value<Boolean> getRelay1() {
return this.getRelay1Channel().value();
}

/**
* Internal method to set the 'nextValue' on {@link ChannelId#RELAY_1} Channel.
*
* @param value the next value
*/
public default void _setRelay1(Boolean value) {
this.getRelay1Channel().setNextValue(value);
}

/**
* Sets the Relay Output 1. See {@link ChannelId#RELAY_1}.
*
* @param value the next write value
* @throws OpenemsNamedException on error
*/
public default void setRelay1(boolean value) throws OpenemsNamedException {
this.getRelay1Channel().setNextWriteValue(value);
}

/**
* Gets the Channel for {@link ChannelId#RELAY_2}.
*
* @return the Channel
*/
public default BooleanWriteChannel getRelay2Channel() {
return this.channel(ChannelId.RELAY_2);
}

/**
* Gets the Relay Output 2. See {@link ChannelId#RELAY_2}.
*
* @return the Channel {@link Value}
*/
public default Value<Boolean> getRelay2() {
return this.getRelay2Channel().value();
}

/**
* Internal method to set the 'nextValue' on {@link ChannelId#RELAY_2} Channel.
*
* @param value the next value
*/
public default void _setRelay2(Boolean value) {
this.getRelay2Channel().setNextValue(value);
}

/**
* Sets the Relay Output 2. See {@link ChannelId#RELAY_2}.
*
* @param value the next write value
* @throws OpenemsNamedException on error
*/
public default void setRelay2(boolean value) throws OpenemsNamedException {
this.getRelay2Channel().setNextWriteValue(value);
}

/**
* Gets the Channel for {@link ChannelId#SLAVE_COMMUNICATION_FAILED}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package io.openems.edge.io.shelly.shelly25;

import static io.openems.common.utils.JsonUtils.getAsBoolean;
import static io.openems.common.utils.JsonUtils.getAsJsonArray;
import static io.openems.common.utils.JsonUtils.getAsJsonObject;

import java.util.Objects;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.osgi.service.event.propertytypes.EventTopics;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonElement;

import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.utils.JsonUtils;
import io.openems.edge.bridge.http.api.BridgeHttp;
import io.openems.edge.bridge.http.api.BridgeHttpFactory;
import io.openems.edge.common.channel.BooleanWriteChannel;
import io.openems.edge.common.channel.WriteChannel;
import io.openems.edge.common.component.AbstractOpenemsComponent;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
Expand All @@ -27,19 +34,23 @@
@Component(//
name = "IO.Shelly.25", //
immediate = true, //
configurationPolicy = ConfigurationPolicy.REQUIRE//
configurationPolicy = ConfigurationPolicy.REQUIRE //
)
@EventTopics({ //
EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, //
EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE //
})

public class IoShelly25Impl extends AbstractOpenemsComponent
implements IoShelly25, DigitalOutput, OpenemsComponent, EventHandler {

private final Logger log = LoggerFactory.getLogger(IoShelly25Impl.class);
private final BooleanWriteChannel[] digitalOutputChannels;

private ShellyApi shellyApi = null;
private String baseUrl;

@Reference
private BridgeHttpFactory httpBridgeFactory;
private BridgeHttp httpBridge;

public IoShelly25Impl() {
super(//
Expand All @@ -56,12 +67,18 @@ public IoShelly25Impl() {
@Activate
private void activate(ComponentContext context, Config config) {
super.activate(context, config.id(), config.alias(), config.enabled());
this.shellyApi = new ShellyApi(config.ip());
this.baseUrl = "http://" + config.ip();
this.httpBridge = this.httpBridgeFactory.get();

if (this.isEnabled()) {
this.httpBridge.subscribeJsonEveryCycle(this.baseUrl + "/status", this::processHttpResult);
}
}

@Override
@Deactivate
protected void deactivate() {
this.httpBridgeFactory.unget(this.httpBridge);
this.httpBridge = null;
super.deactivate();
}

Expand All @@ -72,22 +89,22 @@ public BooleanWriteChannel[] digitalOutputChannels() {

@Override
public String debugLog() {
// TODO share code with AbstractKmtronicRelay.debugLog()
var b = new StringBuilder();
var i = 1;
for (WriteChannel<Boolean> channel : this.digitalOutputChannels) {
for (var channel : this.digitalOutputChannels) {
String valueText;
var valueOpt = channel.value().asOptional();
if (valueOpt.isPresent()) {
valueText = valueOpt.get() ? "x" : "-";
} else {
valueText = "?";
valueText = "Unknown";
}
b.append(i + valueText);

// add space for all but the last
if (++i <= this.digitalOutputChannels.length) {
b.append(" ");
b.append(valueText);
if (i < this.digitalOutputChannels.length) {
b.append("|");
}
i++;
}
return b.toString();
}
Expand All @@ -99,68 +116,84 @@ public void handleEvent(Event event) {
}

switch (event.getTopic()) {
case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE:
this.eventBeforeProcessImage();
break;
case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE //
-> this.eventExecuteWrite();
}
}

case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE:
this.eventExecuteWrite();
break;
private record RelayState(Boolean relayIsOn, Boolean overtemp, Boolean overpower) {
private static RelayState from(JsonElement relay) throws OpenemsNamedException {
var relayIsOn = getAsBoolean(relay, "ison");
var overtemp = getAsBoolean(relay, "overtemperature");
var overpower = getAsBoolean(relay, "overpower");
return new RelayState(relayIsOn, overtemp, overpower);
}

private void applyChannels(IoShelly25 component, IoShelly25.ChannelId relayChannel,
IoShelly25.ChannelId overtempChannel, IoShelly25.ChannelId overpowerChannel) {
component.channel(relayChannel).setNextValue(this.relayIsOn);
component.channel(overtempChannel).setNextValue(this.overtemp);
component.channel(overpowerChannel).setNextValue(this.overpower);
}
}

/**
* Execute on Cycle Event "Before Process Image".
*
* @param result The JSON element containing the result of the HTTP request.
* @param error The throwable error, if any occurred during the HTTP request.
* @throws OpenemsNamedException if the processing of the HTTP result fails or
* communication with the slave device is
* unsuccessful.
*/
private void eventBeforeProcessImage() {
Boolean relay1IsOn;
Boolean relay2IsOn;
try {
var json = this.shellyApi.getStatus();
var relays = JsonUtils.getAsJsonArray(json, "relays");
var relay1 = JsonUtils.getAsJsonObject(relays.get(0));
relay1IsOn = JsonUtils.getAsBoolean(relay1, "ison");
var relay2 = JsonUtils.getAsJsonObject(relays.get(1));
relay2IsOn = JsonUtils.getAsBoolean(relay2, "ison");
private void processHttpResult(JsonElement result, Throwable error) {
var slaveCommunicationFailed = result == null;
var relay1State = new RelayState(null, null, null);
var relay2State = new RelayState(null, null, null);

this._setSlaveCommunicationFailed(false);
try {
final var relays = getAsJsonArray(result, "relays");
relay1State = RelayState.from(getAsJsonObject(relays.get(0)));
relay2State = RelayState.from(getAsJsonObject(relays.get(1)));

} catch (OpenemsNamedException | IndexOutOfBoundsException e) {
relay1IsOn = null;
relay2IsOn = null;
this.logError(this.log, "Unable to read from Shelly API: " + e.getMessage());
this._setSlaveCommunicationFailed(true);
this.logDebug(this.log, e.getMessage());
slaveCommunicationFailed = true;
}
this._setRelay1(relay1IsOn);
this._setRelay2(relay2IsOn);

this._setSlaveCommunicationFailed(slaveCommunicationFailed);
relay1State.applyChannels(this, IoShelly25.ChannelId.RELAY_1, //
IoShelly25.ChannelId.RELAY_1_OVERTEMP, IoShelly25.ChannelId.RELAY_1_OVERPOWER);
relay2State.applyChannels(this, IoShelly25.ChannelId.RELAY_2, //
IoShelly25.ChannelId.RELAY_2_OVERTEMP, IoShelly25.ChannelId.RELAY_2_OVERPOWER);
}

/**
* Execute on Cycle Event "Execute Write".
*/
private void eventExecuteWrite() {
try {
this.executeWrite(this.getRelay1Channel(), 0);
this.executeWrite(this.getRelay2Channel(), 1);

this._setSlaveCommunicationFailed(false);
} catch (OpenemsNamedException e) {
this._setSlaveCommunicationFailed(true);
for (int i = 0; i < this.digitalOutputChannels.length; i++) {
this.executeWrite(this.digitalOutputChannels[i], i);
}
}

private void executeWrite(BooleanWriteChannel channel, int index) throws OpenemsNamedException {
private void executeWrite(BooleanWriteChannel channel, int index) {
var readValue = channel.value().get();
var writeValue = channel.getNextWriteValueAndReset();
if (!writeValue.isPresent()) {
// no write value
if (writeValue.isEmpty()) {
return;
}
if (Objects.equals(readValue, writeValue.get())) {
// read value = write value
return;
}
this.shellyApi.setRelayTurn(index, writeValue.get());
final String url = this.baseUrl + "/relay/" + index + "?turn=" + (writeValue.get() ? "on" : "off");
this.httpBridge.get(url).whenComplete((t, e) -> {
this._setSlaveCommunicationFailed(e != null);
if (e == null) {
this.logInfo(this.log, "Executed write successfully for URL: " + url);
} else {
this.logError(this.log, "Failed to execute write for URL: " + url + "; Error: " + e.getMessage());
}
});
}

}
}
Loading

0 comments on commit 6937f29

Please sign in to comment.