Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored Shelly 2.5 to use HttpBridge #2573

Merged
merged 15 commits into from
Jun 1, 2024
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
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) -> {
Sn0w3y marked this conversation as resolved.
Show resolved Hide resolved
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