diff --git a/backend/src/main/java/io/openems/backend/App.java b/backend/src/main/java/io/openems/backend/App.java index 0a9516467aa..72e14b62be7 100644 --- a/backend/src/main/java/io/openems/backend/App.java +++ b/backend/src/main/java/io/openems/backend/App.java @@ -10,23 +10,28 @@ import io.openems.backend.openemswebsocket.OpenemsWebsocket; import io.openems.backend.restapi.RestApi; import io.openems.backend.timedata.Timedata; +import io.openems.common.exceptions.OpenemsException; import io.openems.common.utils.EnvUtils; public class App { private static Logger log = LoggerFactory.getLogger(App.class); - public static void main(String[] args) throws Exception { + public static void main(String[] args) { log.info("OpenEMS-Backend starting..."); // Configure everything - initMetadataProvider(); - initTimedataProvider(); - initOpenemsWebsocket(); - initBrowserWebsocket(); - initRestApi(); + try { + initMetadataProvider(); + initTimedataProvider(); + initOpenemsWebsocket(); + initBrowserWebsocket(); + initRestApi(); - log.info("OpenEMS Backend started."); - log.info("================================================================================"); + log.info("OpenEMS Backend started."); + log.info("================================================================================"); + } catch (OpenemsException e) { + log.error("OpenEMS Backend failed to start: " + e.getMessage()); + } } /** @@ -34,7 +39,7 @@ public static void main(String[] args) throws Exception { * * @throws Exception */ - private static void initMetadataProvider() throws Exception { + private static void initMetadataProvider() throws OpenemsException { Optional metadataOpt = EnvUtils.getAsOptionalString("METADATA"); if (metadataOpt.isPresent() && metadataOpt.get().equals("DUMMY")) { log.info("Start Dummy Metadata provider"); @@ -50,7 +55,7 @@ private static void initMetadataProvider() throws Exception { } } - private static void initTimedataProvider() throws Exception { + private static void initTimedataProvider() throws OpenemsException { Optional timedataOpt = EnvUtils.getAsOptionalString("TIMEDATA"); if (timedataOpt.isPresent() && timedataOpt.get().equals("DUMMY")) { log.info("Start Dummy Timedata provider"); @@ -66,19 +71,19 @@ private static void initTimedataProvider() throws Exception { } } - private static void initOpenemsWebsocket() throws Exception { + private static void initOpenemsWebsocket() throws OpenemsException { int port = EnvUtils.getAsInt("OPENEMS_WEBSOCKET_PORT"); log.info("Start OpenEMS Websocket server on port [" + port + "]"); OpenemsWebsocket.initialize(port); } - private static void initBrowserWebsocket() throws Exception { + private static void initBrowserWebsocket() throws OpenemsException { int port = EnvUtils.getAsInt("BROWSER_WEBSOCKET_PORT"); log.info("Start Browser Websocket server on port [" + port + "]"); BrowserWebsocket.initialize(port); } - private static void initRestApi() throws Exception { + private static void initRestApi() throws OpenemsException { int port = EnvUtils.getAsInt("REST_API_PORT"); log.info("Start Rest-Api server on port [" + port + "]"); RestApi.initialize(port); diff --git a/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocket.java b/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocket.java index 16356b9e42d..ba89f9ead34 100644 --- a/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocket.java +++ b/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocket.java @@ -1,5 +1,7 @@ package io.openems.backend.browserwebsocket; +import io.openems.common.exceptions.OpenemsException; + /** * Provider for OpenemsWebsocketServer singleton * @@ -16,7 +18,7 @@ public class BrowserWebsocket { * @param port * @throws Exception */ - public static synchronized void initialize(int port) throws Exception { + public static synchronized void initialize(int port) throws OpenemsException { BrowserWebsocket.instance = new BrowserWebsocketSingleton(port); BrowserWebsocket.instance.start(); } diff --git a/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocketSingleton.java b/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocketSingleton.java index a1fcfc646d7..9fd3f037144 100644 --- a/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocketSingleton.java +++ b/backend/src/main/java/io/openems/backend/browserwebsocket/BrowserWebsocketSingleton.java @@ -25,6 +25,7 @@ import io.openems.common.utils.JsonUtils; import io.openems.common.websocket.AbstractWebsocketServer; import io.openems.common.websocket.DefaultMessages; +import io.openems.common.websocket.LogBehaviour; import io.openems.common.websocket.Notification; import io.openems.common.websocket.WebSocketUtils; @@ -38,7 +39,7 @@ public class BrowserWebsocketSingleton extends AbstractWebsocketServer { private final Logger log = LoggerFactory.getLogger(BrowserWebsocketSingleton.class); - protected BrowserWebsocketSingleton(int port) throws Exception { + protected BrowserWebsocketSingleton(int port) throws OpenemsException { super(port, new BrowserSessionManager()); } @@ -89,7 +90,7 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { // check if the session is now valid and send reply to browser BrowserSessionData data = session.getData(); - if (error.isEmpty() && session.isValid()) { + if (error.isEmpty()) { // add isOnline information OpenemsWebsocketSingleton openemsWebsocket = OpenemsWebsocket.instance(); for (Device device : data.getDevices()) { @@ -103,18 +104,17 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { WebSocketUtils.send(websocket, jReply); // add websocket to local cache - this.websockets.forcePut(websocket, session); + this.addWebsocket(websocket, session); log.info("User [" + data.getUserName() + "] connected with Session [" + data.getOdooSessionId().orElse("") - + "]. Total websockets [" + this.websockets.size() + "]"); + + "]."); } else { // send connection failed to browser JsonObject jReply = DefaultMessages.browserConnectionFailedReply(); WebSocketUtils.send(websocket, jReply); log.warn("User [" + data.getUserName() + "] connection failed. Session [" - + data.getOdooSessionId().orElse("") + "] Error [" + error + "]. Total websockets [" - + this.websockets.size() + "]"); + + data.getOdooSessionId().orElse("") + "] Error [" + error + "]."); websocket.closeConnection(CloseFrame.REFUSE, error); } @@ -158,9 +158,8 @@ protected void _onMessage(WebSocket websocket, JsonObject jMessage, Optional session = this.getSessionFromWebsocket(websocket); JsonArray jId; if (jMessage.has("id")) { jId = JsonUtils.getAsJsonArray(jMessage, "id"); } else { jId = new JsonArray(); } - jId.add(session.getToken()); + jId.add(session.get().getToken()); jMessage.add("id", jId); // get OpenEMS websocket and forward message @@ -251,8 +250,9 @@ public void openemsConnectionClosed(String name) { for (BrowserSession session : this.sessionManager.getSessions()) { for (Device device : session.getData().getDevices()) { if (name.equals(device.getName())) { - WebSocket websocket = this.websockets.inverse().get(session); - WebSocketUtils.sendNotification(websocket, Notification.EDGE_CONNECTION_ClOSED, name); + Optional websocketOpt = this.getWebsocketFromSession(session); + WebSocketUtils.sendNotification(websocketOpt, LogBehaviour.DO_NOT_WRITE_TO_LOG, + Notification.EDGE_CONNECTION_ClOSED, name); } } } @@ -267,9 +267,9 @@ public void openemsConnectionOpened(String name) { for (BrowserSession session : this.sessionManager.getSessions()) { for (Device device : session.getData().getDevices()) { if (name.equals(device.getName())) { - WebSocket websocket = this.websockets.inverse().get(session); - WebSocketUtils.sendNotification(Optional.ofNullable(websocket), Notification.EDGE_CONNECTION_OPENED, - name); + Optional websocketOpt = this.getWebsocketFromSession(session); + WebSocketUtils.sendNotification(websocketOpt, LogBehaviour.DO_NOT_WRITE_TO_LOG, + Notification.EDGE_CONNECTION_OPENED, name); } } } diff --git a/backend/src/main/java/io/openems/backend/metadata/Metadata.java b/backend/src/main/java/io/openems/backend/metadata/Metadata.java index c0dd8b67d48..890624b62aa 100644 --- a/backend/src/main/java/io/openems/backend/metadata/Metadata.java +++ b/backend/src/main/java/io/openems/backend/metadata/Metadata.java @@ -3,6 +3,7 @@ import io.openems.backend.metadata.api.MetadataSingleton; import io.openems.backend.metadata.dummy.MetadataDummySingleton; import io.openems.backend.metadata.odoo.OdooSingleton; +import io.openems.common.exceptions.OpenemsException; /** * Provider for Metadata singleton @@ -21,9 +22,9 @@ public class Metadata { * @throws Exception */ public static synchronized void initializeOdoo(String url, int port, String database, String username, - String password) throws Exception { + String password) throws OpenemsException { if (url == null || database == null || username == null || password == null) { - throw new Exception("Config missing: database [" + database + "], url [" + url + "], port [" + port + throw new OpenemsException("Config missing: database [" + database + "], url [" + url + "], port [" + port + "] username [" + username + "], password [" + password + "]"); } Metadata.instance = new OdooSingleton(url, port, database, username, password); diff --git a/backend/src/main/java/io/openems/backend/metadata/dummy/MetadataDummySingleton.java b/backend/src/main/java/io/openems/backend/metadata/dummy/MetadataDummySingleton.java index 5b5ce257a16..77e2ffeb34e 100644 --- a/backend/src/main/java/io/openems/backend/metadata/dummy/MetadataDummySingleton.java +++ b/backend/src/main/java/io/openems/backend/metadata/dummy/MetadataDummySingleton.java @@ -38,7 +38,6 @@ public void getInfoWithSession(BrowserSession session) throws OpenemsException { deviceInfos.add(device); } data.setDevices(deviceInfos); - session.setValid(); return; } diff --git a/backend/src/main/java/io/openems/backend/metadata/odoo/OdooSingleton.java b/backend/src/main/java/io/openems/backend/metadata/odoo/OdooSingleton.java index 7ec0c4d9216..308a2630218 100644 --- a/backend/src/main/java/io/openems/backend/metadata/odoo/OdooSingleton.java +++ b/backend/src/main/java/io/openems/backend/metadata/odoo/OdooSingleton.java @@ -11,6 +11,9 @@ import java.util.ArrayList; import java.util.List; +import org.apache.xmlrpc.XmlRpcException; + +import com.abercap.odoo.OdooApiException; import com.abercap.odoo.Session; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -32,15 +35,24 @@ public class OdooSingleton implements MetadataSingleton { private MetadataDeviceModel deviceModel; private final String url; - public OdooSingleton(String url, int port, String database, String username, String password) throws Exception { + public OdooSingleton(String url, int port, String database, String username, String password) + throws OpenemsException { this.session = new Session(url, port, database, username, password); this.connect(); - this.deviceModel = new OdooDeviceModel(this.session); + try { + this.deviceModel = new OdooDeviceModel(this.session); + } catch (XmlRpcException | OdooApiException e) { + throw new OpenemsException("Initializing OdooDeviceModel failed: " + e.getMessage()); + } this.url = "http://" + url + ":" + port; } - private void connect() throws Exception { - session.startSession(); + private void connect() throws OpenemsException { + try { + session.startSession(); + } catch (Exception e) { + throw new OpenemsException("Odoo connection failed: " + e.getMessage()); + } } @Override @@ -113,7 +125,6 @@ public void getInfoWithSession(BrowserSession session) throws OpenemsException { JsonUtils.getAsString(jDevice, "role"))); } data.setDevices(deviceInfos); - session.setValid(); return; } } diff --git a/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocket.java b/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocket.java index 2f45e8086de..17ae2bd983b 100644 --- a/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocket.java +++ b/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocket.java @@ -16,7 +16,7 @@ public class OpenemsWebsocket { * @param port * @throws Exception */ - public static synchronized void initialize(int port) throws Exception { + public static synchronized void initialize(int port) { OpenemsWebsocket.instance = new OpenemsWebsocketSingleton(port); OpenemsWebsocket.instance.start(); } diff --git a/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocketSingleton.java b/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocketSingleton.java index 4ba3edd497b..d2ca7765243 100644 --- a/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocketSingleton.java +++ b/backend/src/main/java/io/openems/backend/openemswebsocket/OpenemsWebsocketSingleton.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.Map.Entry; import java.util.Optional; import org.java_websocket.WebSocket; @@ -38,7 +39,7 @@ public class OpenemsWebsocketSingleton extends AbstractWebsocketServer { private final Logger log = LoggerFactory.getLogger(OpenemsWebsocketSingleton.class); - protected OpenemsWebsocketSingleton(int port) throws Exception { + protected OpenemsWebsocketSingleton(int port) { super(port, new OpenemsSessionManager()); } @@ -58,6 +59,16 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { } apikey = apikeyOpt.get(); + // if existing: close existing websocket for this apikey + Optional oldSessionOpt = this.sessionManager.getSessionByToken(apikey); + if (oldSessionOpt.isPresent()) { + OpenemsSession oldSession = oldSessionOpt.get(); + WebSocket oldWebsocket = oldSession.getData().getWebsocket(); + oldWebsocket.closeConnection(CloseFrame.REFUSE, + "Another device with this apikey [" + apikey + "] connected."); + this.sessionManager.removeSession(oldSession); + } + // get device for apikey Optional deviceOpt = Metadata.instance().getDeviceModel().getDeviceForApikey(apikey); if (!deviceOpt.isPresent()) { @@ -67,17 +78,16 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { deviceName = device.getName(); // create new session - OpenemsSessionData sessionData = new OpenemsSessionData(device); + OpenemsSessionData sessionData = new OpenemsSessionData(websocket, device); OpenemsSession session = sessionManager.createNewSession(apikey, sessionData); - session.setValid(); // send successful reply to openems JsonObject jReply = DefaultMessages.openemsConnectionSuccessfulReply(); WebSocketUtils.send(websocket, jReply); // add websocket to local cache - this.websockets.forcePut(websocket, session); + this.addWebsocket(websocket, session); - log.info("Device [" + deviceName + "] connected. Total websockets [" + this.websockets.size() + "]"); + log.info("Device [" + deviceName + "] connected."); try { // set device active (in Odoo) @@ -110,8 +120,8 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { @Override public void _onClose(WebSocket websocket, Optional sessionOpt) { if (sessionOpt.isPresent()) { - log.info("Would remove the session... " + sessionOpt.get()); - // TODO sessionManager.removeSession(sessionOpt.get()); + // log.info("Would remove the session... " + sessionOpt.get()); + sessionManager.removeSession(sessionOpt.get()); } } @@ -121,14 +131,30 @@ public void _onClose(WebSocket websocket, Optional sessionOpt) { @Override protected void _onMessage(WebSocket websocket, JsonObject jMessage, Optional jMessageIdOpt, Optional deviceNameOpt) { - MetadataDevice device = websockets.get(websocket).getData().getDevice(); + MetadataDevice device = this.getSessionFromWebsocket(websocket).get().getData().getDevice(); // if (!jMessage.has("timedata") && !jMessage.has("currentData") && !jMessage.has("log") // && !jMessage.has("config")) { // log.info("Received from " + device.getName() + ": " + jMessage.toString()); // } - // Is this a reply? + /* + * Config? -> store in Metadata + */ + if (jMessage.has("config")) { + try { + JsonObject jConfig = JsonUtils.getAsJsonObject(jMessage, "config"); + device.setOpenemsConfig(jConfig); + device.writeObject(); + log.info("Device [" + device.getName() + "] sent config."); + } catch (OpenemsException e) { + log.error(e.getMessage()); + } + } + + /* + * Is this a reply? -> forward to Browser + */ if (jMessage.has("id")) { forwardReplyToBrowser(websocket, device.getName(), jMessage); } @@ -187,19 +213,29 @@ private void timedata(MetadataDevice device, JsonElement jTimedataElement) { JsonObject jTimedata = JsonUtils.getAsJsonObject(jTimedataElement); // Write to InfluxDB try { - Timedata.instance().write(device.getNameNumber(), jTimedata); + Timedata.instance().write(device, jTimedata); log.debug(device.getName() + ": wrote " + jTimedata.entrySet().size() + " timestamps " + StringUtils.toShortString(jTimedata, 120)); } catch (Exception e) { log.error("Unable to write Timedata: ", e); } - // Write some data to Odoo - // This is only to provide feedback for FENECON Service-Team that the device is online. - device.setLastUpdate(); + // Set Odoo last message timestamp device.setLastMessage(); - jTimedata.entrySet().forEach(entry -> { + + for (Entry jTimedataEntry : jTimedata.entrySet()) { try { - JsonObject jChannels = JsonUtils.getAsJsonObject(entry.getValue()); + JsonObject jChannels = JsonUtils.getAsJsonObject(jTimedataEntry.getValue()); + + // set Odoo last update timestamp only for those channels + for (String channel : jChannels.keySet()) { + if (channel.endsWith("ActivePower") + || channel.endsWith("ActivePowerL1") | channel.endsWith("ActivePowerL2") + | channel.endsWith("ActivePowerL3") | channel.endsWith("Soc")) { + device.setLastUpdate(); + } + } + + // set specific Odoo values if (jChannels.has("ess0/Soc")) { int soc = JsonUtils.getAsPrimitive(jChannels, "ess0/Soc").getAsInt(); device.setSoc(soc); @@ -211,7 +247,8 @@ private void timedata(MetadataDevice device, JsonElement jTimedataElement) { } catch (OpenemsException e) { log.error("Device [" + device.getName() + "] error: " + e.getMessage()); } - }); + } + } catch (OpenemsException e) { log.error("Device [" + device.getName() + "] error: " + e.getMessage()); } @@ -257,7 +294,7 @@ public Optional getOpenemsWebsocket(String deviceName) { return Optional.empty(); } OpenemsSession session = sessionOpt.get(); - return Optional.ofNullable(this.websockets.inverse().get(session)); + return this.getWebsocketFromSession(session); } public Collection getSessions() { diff --git a/backend/src/main/java/io/openems/backend/openemswebsocket/session/OpenemsSessionData.java b/backend/src/main/java/io/openems/backend/openemswebsocket/session/OpenemsSessionData.java index ca509c09d8c..d2ed541c5ae 100644 --- a/backend/src/main/java/io/openems/backend/openemswebsocket/session/OpenemsSessionData.java +++ b/backend/src/main/java/io/openems/backend/openemswebsocket/session/OpenemsSessionData.java @@ -1,5 +1,7 @@ package io.openems.backend.openemswebsocket.session; +import org.java_websocket.WebSocket; + import com.google.gson.JsonObject; import io.openems.backend.metadata.api.device.MetadataDevice; @@ -7,9 +9,15 @@ public class OpenemsSessionData extends SessionData { private final MetadataDevice device; + private final WebSocket websocket; - public OpenemsSessionData(MetadataDevice device) { + public OpenemsSessionData(WebSocket websocket, MetadataDevice device) { this.device = device; + this.websocket = websocket; + } + + public WebSocket getWebsocket() { + return websocket; } public MetadataDevice getDevice() { diff --git a/backend/src/main/java/io/openems/backend/restapi/RestApi.java b/backend/src/main/java/io/openems/backend/restapi/RestApi.java index 66a5048ca6e..62340b9200d 100644 --- a/backend/src/main/java/io/openems/backend/restapi/RestApi.java +++ b/backend/src/main/java/io/openems/backend/restapi/RestApi.java @@ -1,5 +1,7 @@ package io.openems.backend.restapi; +import io.openems.common.exceptions.OpenemsException; + /** * Provider for RestApiSingleton singleton * @@ -16,7 +18,7 @@ public class RestApi { * @param port * @throws Exception */ - public static synchronized void initialize(int port) throws Exception { + public static synchronized void initialize(int port) throws OpenemsException { RestApi.instance = new RestApiSingleton(port); } diff --git a/backend/src/main/java/io/openems/backend/restapi/RestApiSingleton.java b/backend/src/main/java/io/openems/backend/restapi/RestApiSingleton.java index a80383d54c2..b6ec6b33afc 100644 --- a/backend/src/main/java/io/openems/backend/restapi/RestApiSingleton.java +++ b/backend/src/main/java/io/openems/backend/restapi/RestApiSingleton.java @@ -5,16 +5,22 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.openems.common.exceptions.OpenemsException; + public class RestApiSingleton { private final Logger log = LoggerFactory.getLogger(RestApiSingleton.class); private final Component component; - public RestApiSingleton(int port) throws Exception { + public RestApiSingleton(int port) throws OpenemsException { this.component = new Component(); this.component.getServers().add(Protocol.HTTP, port); this.component.getDefaultHost().attach("/rest", new RestApiApplication()); - this.component.start(); + try { + this.component.start(); + } catch (Exception e) { + throw new OpenemsException("Starting REST-Api failed: " + e.getMessage()); + } log.info("REST-Api started on port [" + port + "]."); } } diff --git a/backend/src/main/java/io/openems/backend/timedata/Timedata.java b/backend/src/main/java/io/openems/backend/timedata/Timedata.java index 8c6cfb9985e..d56dc810087 100644 --- a/backend/src/main/java/io/openems/backend/timedata/Timedata.java +++ b/backend/src/main/java/io/openems/backend/timedata/Timedata.java @@ -3,6 +3,7 @@ import io.openems.backend.timedata.api.TimedataSingleton; import io.openems.backend.timedata.dummy.TimedataDummySingleton; import io.openems.backend.timedata.influx.InfluxdbSingleton; +import io.openems.common.exceptions.OpenemsException; /** * Provider for Timedata singleton @@ -21,9 +22,9 @@ public class Timedata { * @throws Exception */ public static void initializeInfluxdb(String database, String url, int port, String username, String password) - throws Exception { + throws OpenemsException { if (database == null || url == null || username == null || password == null) { - throw new Exception("Config missing: database [" + database + "], url [" + url + "], port [" + port + throw new OpenemsException("Config missing: database [" + database + "], url [" + url + "], port [" + port + "] username [" + username + "], password [" + password + "]"); } Timedata.instance = new InfluxdbSingleton(database, url, port, username, password); diff --git a/backend/src/main/java/io/openems/backend/timedata/api/TimedataSingleton.java b/backend/src/main/java/io/openems/backend/timedata/api/TimedataSingleton.java index 68da7b15237..6a93d2be15a 100644 --- a/backend/src/main/java/io/openems/backend/timedata/api/TimedataSingleton.java +++ b/backend/src/main/java/io/openems/backend/timedata/api/TimedataSingleton.java @@ -1,9 +1,8 @@ package io.openems.backend.timedata.api; -import java.util.Optional; - import com.google.gson.JsonObject; +import io.openems.backend.metadata.api.device.MetadataDevice; import io.openems.common.api.TimedataSource; public interface TimedataSingleton extends TimedataSource { @@ -23,5 +22,5 @@ public interface TimedataSingleton extends TimedataSource { * } * */ - public void write(Optional deviceId, JsonObject jData); + public void write(MetadataDevice device, JsonObject jData); } diff --git a/backend/src/main/java/io/openems/backend/timedata/dummy/TimedataDummySingleton.java b/backend/src/main/java/io/openems/backend/timedata/dummy/TimedataDummySingleton.java index 110b4fa377c..786f8efd3a9 100644 --- a/backend/src/main/java/io/openems/backend/timedata/dummy/TimedataDummySingleton.java +++ b/backend/src/main/java/io/openems/backend/timedata/dummy/TimedataDummySingleton.java @@ -9,6 +9,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import io.openems.backend.metadata.api.device.MetadataDevice; import io.openems.backend.timedata.api.TimedataSingleton; import io.openems.backend.utilities.StringUtils; import io.openems.common.exceptions.OpenemsException; @@ -17,7 +18,7 @@ public class TimedataDummySingleton implements TimedataSingleton { private final Logger log = LoggerFactory.getLogger(TimedataDummySingleton.class); @Override - public void write(Optional deviceId, JsonObject jData) { + public void write(MetadataDevice device, JsonObject jData) { log.debug("Timedata Dummy. Would write data: " + StringUtils.toShortString(jData, 100)); } diff --git a/backend/src/main/java/io/openems/backend/timedata/influx/InfluxdbSingleton.java b/backend/src/main/java/io/openems/backend/timedata/influx/InfluxdbSingleton.java index d49093dfe2b..f194b76cc18 100644 --- a/backend/src/main/java/io/openems/backend/timedata/influx/InfluxdbSingleton.java +++ b/backend/src/main/java/io/openems/backend/timedata/influx/InfluxdbSingleton.java @@ -3,6 +3,7 @@ import java.text.NumberFormat; import java.text.ParseException; import java.time.ZonedDateTime; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; @@ -21,11 +22,13 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.google.common.collect.Tables; +import com.google.common.collect.TreeBasedTable; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import io.openems.backend.metadata.api.device.MetadataDevice; import io.openems.backend.timedata.api.TimedataSingleton; import io.openems.common.exceptions.OpenemsException; import io.openems.common.utils.InfluxdbUtils; @@ -35,6 +38,9 @@ public class InfluxdbSingleton implements TimedataSingleton { private final Logger log = LoggerFactory.getLogger(InfluxdbSingleton.class); + private final String MEASUREMENT = "data"; + private final String TMP_MINI_MEASUREMENT = "minies"; + private String database; private String url; private int port; @@ -47,13 +53,18 @@ public class InfluxdbSingleton implements TimedataSingleton { // key: deviceId; value: timestamp private Map lastTimestampMap = new ConcurrentHashMap(); - public InfluxdbSingleton(String database, String url, int port, String username, String password) throws Exception { + public InfluxdbSingleton(String database, String url, int port, String username, String password) + throws OpenemsException { this.database = database; this.url = url; this.port = port; this.username = username; this.password = password; - this.connect(); + try { + this.connect(); + } catch (Exception e) { + throw new OpenemsException("Connecting to InfluxDB failed: " + e.getMessage()); + } } private void connect() throws Exception { @@ -82,65 +93,151 @@ private void connect() throws Exception { * "timestamp2" { "channel1": value, "channel2": value } } */ @Override - public void write(Optional deviceIdOpt, JsonObject jData) { - int deviceId = deviceIdOpt.orElse(0); + public void write(MetadataDevice device, JsonObject jData) { + int deviceId = device.getNameNumber().orElse(0); long lastTimestamp = this.lastTimestampMap.getOrDefault(deviceId, 0l); - BatchPoints batchPoints = BatchPoints.database(database) // - .tag("fems", String.valueOf(deviceId)) // - .build(); - // Sort data by timestamp - TreeMap data = new TreeMap(); - jData.entrySet().forEach(timestampEntry -> { - String timestampString = timestampEntry.getKey(); - Long timestamp = Long.valueOf(timestampString); - JsonObject jChannels; + TreeMap sortedData = new TreeMap(); + for (Entry entry : jData.entrySet()) { try { - jChannels = JsonUtils.getAsJsonObject(timestampEntry.getValue()); - data.put(timestamp, jChannels); + Long timestamp = Long.valueOf(entry.getKey()); + JsonObject jChannels; + jChannels = JsonUtils.getAsJsonObject(entry.getValue()); + sortedData.put(timestamp, jChannels); } catch (OpenemsException e) { log.error("Data error: " + e.getMessage()); } - }); + } - // Prepare data for writing to InfluxDB - for (Entry dataEntry : data.entrySet()) { + // Prepare data table + TreeBasedTable data = TreeBasedTable.create(); + for (Entry dataEntry : sortedData.entrySet()) { Long timestamp = dataEntry.getKey(); // use lastDataCache only if we receive the latest data and cache is not elder than 1 minute boolean useLastDataCache = timestamp > lastTimestamp && timestamp < lastTimestamp + 60000; this.lastTimestampMap.put(deviceId, timestamp); - Builder builder = Point.measurement("data") // this builds an InfluxDB record ("point") for a given - // timestamp - .time(timestamp, TimeUnit.MILLISECONDS); - JsonObject jChannels = dataEntry.getValue(); + if (jChannels.entrySet().size() > 0) { - jChannels.entrySet().forEach(channelEntry -> { + for (Entry channelEntry : jChannels.entrySet()) { String channel = channelEntry.getKey(); - Optional valueOpt = this.addChannelToBuilder(builder, channel, channelEntry.getValue()); - if (valueOpt.isPresent() && useLastDataCache) { - this.lastDataCache.put(deviceId, channel, valueOpt.get()); + Optional valueOpt = this.parseValue(channel, channelEntry.getValue()); + if (valueOpt.isPresent()) { + Object value = valueOpt.get(); + data.put(timestamp, channel, value); + if (useLastDataCache) { + this.lastDataCache.put(deviceId, channel, value); + } } - - }); + } // only for latest data: add the cached data to the InfluxDB point. if (useLastDataCache) { this.lastDataCache.row(deviceId).entrySet().forEach(cacheEntry -> { String channel = cacheEntry.getKey(); - Object value = cacheEntry.getValue(); - this.addChannelToBuilder(builder, channel, value); + Optional valueOpt = this.parseValue(channel, cacheEntry.getValue()); + if (valueOpt.isPresent()) { + Object value = valueOpt.get(); + data.put(timestamp, channel, value); + } }); } - // add the point to the batch - batchPoints.point(builder.build()); - // set last timestamp lastTimestamp = timestamp; } } + + // Write data to default location + writeData(device, data); + + // Hook to continue writing data to old Mini monitoring + // TODO remove after full migration + if (device.getProductType().equals("MiniES 3-3")) { + writeDataToOldMiniMonitoring(device, data); + } + } + + private void writeData(MetadataDevice device, TreeBasedTable data) { + int deviceId = device.getNameNumber().orElse(0); + BatchPoints batchPoints = BatchPoints.database(database) // + .tag("fems", String.valueOf(deviceId)) // + .build(); + + for (Entry> entry : data.rowMap().entrySet()) { + Long timestamp = entry.getKey(); + Builder builder = Point.measurement(MEASUREMENT) // this builds an InfluxDB record ("point") for a given + // timestamp + .time(timestamp, TimeUnit.MILLISECONDS).fields(entry.getValue()); + batchPoints.point(builder.build()); + } + + // write to DB + influxDB.write(batchPoints); + } + + /** + * Writes data to old database for old Mini monitoring + * TODO remove after full migration + * + * @param device + * @param data + */ + private void writeDataToOldMiniMonitoring(MetadataDevice device, TreeBasedTable data) { + int deviceId = device.getNameNumber().orElse(0); + BatchPoints batchPoints = BatchPoints.database(database) // + .tag("fems", String.valueOf(deviceId)) // + .build(); + + for (Entry> entry : data.rowMap().entrySet()) { + Long timestamp = entry.getKey(); + Builder builder = Point.measurement(TMP_MINI_MEASUREMENT).time(timestamp, TimeUnit.MILLISECONDS); + + Map fields = new HashMap<>(); + + for (Entry valueEntry : entry.getValue().entrySet()) { + String channel = valueEntry.getKey(); + Object valueObj = valueEntry.getValue(); + if (valueObj instanceof Number) { + Long value = ((Number) valueObj).longValue(); + + // convert channel ids to old identifiers + if (channel.equals("ess0/Soc")) { + fields.put("Stack_SOC", value); + device.setSoc(value.intValue()); + } else if (channel.equals("meter0/ActivePower")) { + fields.put("PCS_Grid_Power_Total", value * -1); + } else if (channel.equals("meter1/ActivePower")) { + fields.put("PCS_PV_Power_Total", value); + } else if (channel.equals("meter2/ActivePower")) { + fields.put("PCS_Load_Power_Total", value); + } + + // from here value needs to be divided by 10 for backwards compatibility + value = value / 10; + if (channel.equals("meter2/Energy")) { + fields.put("PCS_Summary_Consumption_Accumulative_cor", value); + fields.put("PCS_Summary_Consumption_Accumulative", value); + } else if (channel.equals("meter0/BuyFromGridEnergy")) { + fields.put("PCS_Summary_Grid_Buy_Accumulative_cor", value); + fields.put("PCS_Summary_Grid_Buy_Accumulative", value); + } else if (channel.equals("meter0/SellToGridEnergy")) { + fields.put("PCS_Summary_Grid_Sell_Accumulative_cor", value); + fields.put("PCS_Summary_Grid_Sell_Accumulative", value); + } else if (channel.equals("meter1/EnergyL1")) { + fields.put("PCS_Summary_PV_Accumulative_cor", value); + fields.put("PCS_Summary_PV_Accumulative", value); + } + } + } + + if (fields.size() > 0) { + builder.fields(fields); + batchPoints.point(builder.build()); + } + } + // write to DB influxDB.write(batchPoints); } @@ -153,7 +250,7 @@ public void write(Optional deviceIdOpt, JsonObject jData) { * @param value * @return */ - private Optional addChannelToBuilder(Builder builder, String channel, Object value) { + private Optional parseValue(String channel, Object value) { if (value == null) { return Optional.empty(); } @@ -180,21 +277,16 @@ private Optional addChannelToBuilder(Builder builder, String channel, Ob if (value instanceof Number) { Number numberValue = (Number) value; if (numberValue instanceof Integer) { - builder.addField(channel, numberValue.intValue()); return Optional.of(numberValue.intValue()); } else if (numberValue instanceof Double) { - builder.addField(channel, numberValue.doubleValue()); return Optional.of(numberValue.doubleValue()); } else { - builder.addField(channel, numberValue); return Optional.of(numberValue); } } else if (value instanceof Boolean) { - builder.addField(channel, (Boolean) value); - return Optional.of(value); + return Optional.of((Boolean) value); } else if (value instanceof String) { - builder.addField(channel, (String) value); - return Optional.of(value); + return Optional.of((String) value); } log.warn("Unknown type of value [" + value + "] channel [" + channel + "]. This should never happen."); return Optional.empty(); diff --git a/common/src/io/openems/common/session/Session.java b/common/src/io/openems/common/session/Session.java index 0aeba0bda2e..f4fded80fac 100644 --- a/common/src/io/openems/common/session/Session.java +++ b/common/src/io/openems/common/session/Session.java @@ -2,7 +2,6 @@ public class Session { private final String token; - private boolean valid = false; /** * store additional metadata to this session @@ -18,24 +17,12 @@ public String getToken() { return token; } - public void setValid() { - this.valid = true; - } - - public void setInvalid() { - this.valid = false; - } - - public boolean isValid() { - return this.valid; - } - public D getData() { return data; } @Override public String toString() { - return "Session [token=" + token + ", valid=" + valid + ", data=" + data + "]"; + return "Session [token=" + token + ", data=" + data + "]"; } } diff --git a/common/src/io/openems/common/session/SessionManager.java b/common/src/io/openems/common/session/SessionManager.java index 2db8a1ad53b..3550efb1c39 100644 --- a/common/src/io/openems/common/session/SessionManager.java +++ b/common/src/io/openems/common/session/SessionManager.java @@ -28,55 +28,63 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.openems.common.utils.SecureRandomSingleton; public abstract class SessionManager, D extends SessionData> { + private final Logger log = LoggerFactory.getLogger(SessionManager.class); private final static int SESSION_ID_LENGTH = 130; - // TODO: invalidate old sessions in separate thread: call _removeSession to do so + // TODO: invalidate old sessions in separate thread: call _removeSession to do + // so private final Map sessions = new ConcurrentHashMap<>(); - - protected SessionManager() {} - + + protected SessionManager() { + } + public S createNewSession(String token, D data) { S session = this._createNewSession(token, data); this._putSession(token, session); return session; } - + public S createNewSession(D data) { String token = this.generateToken(); return this.createNewSession(token, data); } - + public Optional getSessionByToken(String token) { - return Optional.ofNullable(this.sessions.get(token)); + synchronized (this.sessions) { + return Optional.ofNullable(this.sessions.get(token)); + } } - + public void removeSession(String token) { - S session = this.sessions.get(token); - if(session != null) { - session.setInvalid(); - this._removeSession(token); + synchronized (this.sessions) { + S session = this.sessions.get(token); + if (session != null) { + this._removeSession(token); + } } } - + public void removeSession(Session session) { - session.setInvalid(); this.removeSession(session.getToken()); } - + protected String generateToken() { // Source: http://stackoverflow.com/a/41156 SecureRandom sr = SecureRandomSingleton.getInstance(); return new BigInteger(SESSION_ID_LENGTH, sr).toString(32); } - + public Collection getSessions() { return Collections.unmodifiableCollection(this.sessions.values()); } - + /* * Those methods are prone to be overwritten by inheritance */ @@ -84,28 +92,35 @@ public Collection getSessions() { * Replies a Session object of type T * * @param token + * @param websocket * @param data * @return */ protected abstract S _createNewSession(String token, D data); - + /** - * This method is always called when adding a session to local database + * This method is always called when adding a session to local database * * @param token * @param session */ protected void _putSession(String token, S session) { - this.sessions.put(token, session); + synchronized (this.sessions) { + if (this.sessions.containsKey(token)) { + log.warn("Session with token [" + token + "] already existed. Replacing with session [" + session + "]"); + } + this.sessions.put(token, session); + } } - + /** - * This method is always called when removing a session from local database + * This method is always called when removing a session from local database * - * @param token * @param session */ protected void _removeSession(String token) { - this.sessions.remove(token); + synchronized (this.sessions) { + this.sessions.remove(token); + } } } diff --git a/common/src/io/openems/common/websocket/AbstractWebsocketServer.java b/common/src/io/openems/common/websocket/AbstractWebsocketServer.java index 5b34311e9d4..940b572b3c6 100644 --- a/common/src/io/openems/common/websocket/AbstractWebsocketServer.java +++ b/common/src/io/openems/common/websocket/AbstractWebsocketServer.java @@ -26,12 +26,13 @@ public abstract class AbstractWebsocketServer, D extends Se extends WebSocketServer { private final Logger log = LoggerFactory.getLogger(AbstractWebsocketServer.class); protected final M sessionManager; - protected final BiMap websockets = Maps.synchronizedBiMap(HashBiMap.create()); + private final BiMap websockets = Maps.synchronizedBiMap(HashBiMap.create()); protected abstract void _onMessage(WebSocket websocket, JsonObject jMessage, Optional jMessageIdOpt, Optional deviceNameOpt); protected abstract void _onOpen(WebSocket websocket, ClientHandshake handshake); + protected abstract void _onClose(WebSocket websocket, Optional sessionOpt); public AbstractWebsocketServer(int port, M sessionManager) { @@ -53,7 +54,8 @@ public final void onOpen(WebSocket websocket, ClientHandshake handshake) { } /** - * Close event of websocket. Removes the websocket. Keeps the session. Calls _onClose() + * Close event of websocket. Removes the websocket. Keeps the session. Calls + * _onClose() */ @Override public final void onClose(WebSocket websocket, int code, String reason, boolean remote) { @@ -158,4 +160,31 @@ public Optional getWebsocketByToken(String token) { return Optional.ofNullable(this.websockets.inverse().get(session)); } + protected void addWebsocket(WebSocket websocket, S session) { + synchronized (this.websockets) { + if (this.websockets.containsKey(websocket)) { + log.warn("Websocket [" + websocket + "] already existed. Replacing existing one with session [" + + session + "]"); + } + if (this.websockets.inverse().containsKey(session)) { + log.warn("Session [" + session + "] already existed. Replacing existing one with websocket [" + + websocket + "]"); + } + this.websockets.forcePut(websocket, session); + } + } + + protected void removeWebsocket(WebSocket websocket) { + synchronized (this.websockets) { + this.websockets.remove(websocket); + } + } + + protected Optional getSessionFromWebsocket(WebSocket websocket) { + return Optional.ofNullable(this.websockets.get(websocket)); + } + + protected Optional getWebsocketFromSession(S session) { + return Optional.ofNullable(this.websockets.inverse().get(session)); + } } diff --git a/common/src/io/openems/common/websocket/LogBehaviour.java b/common/src/io/openems/common/websocket/LogBehaviour.java new file mode 100644 index 00000000000..c74395a7b5c --- /dev/null +++ b/common/src/io/openems/common/websocket/LogBehaviour.java @@ -0,0 +1,5 @@ +package io.openems.common.websocket; + +public enum LogBehaviour { + WRITE_TO_LOG, DO_NOT_WRITE_TO_LOG; +} diff --git a/common/src/io/openems/common/websocket/WebSocketUtils.java b/common/src/io/openems/common/websocket/WebSocketUtils.java index 7e4f798d8b6..5c73f38c48e 100644 --- a/common/src/io/openems/common/websocket/WebSocketUtils.java +++ b/common/src/io/openems/common/websocket/WebSocketUtils.java @@ -19,7 +19,7 @@ import io.openems.common.utils.StringUtils; public class WebSocketUtils { - + private static Logger log = LoggerFactory.getLogger(WebSocketUtils.class); public static boolean send(Optional websocketOpt, JsonObject j) { @@ -31,32 +31,35 @@ public static boolean send(Optional websocketOpt, JsonObject j) { } } - public static boolean sendNotification(Optional websocketOpt, Notification code, Object... params) { + public static boolean sendNotification(Optional websocketOpt, LogBehaviour logBehaviour, Notification code, + Object... params) { if (!websocketOpt.isPresent()) { - log.error("Websocket is not available. Unable to send Notification [" + String.format(code.getMessage(), params) + "]"); + log.error("Websocket is not available. Unable to send Notification [" + + String.format(code.getMessage(), params) + "]"); return false; } else { - return WebSocketUtils.sendNotification(websocketOpt.get(), code, params); + return WebSocketUtils.sendNotification(websocketOpt.get(), logBehaviour, code, params); } } - - public static boolean sendNotification(WebSocket websocket, Notification code, Object... params) { + public static boolean sendNotification(WebSocket websocket, LogBehaviour logBehaviour, Notification code, Object... params) { String message = String.format(code.getMessage(), params); String logMessage = "Notification [" + code.getValue() + "]: " + message; // log message - switch (code.getStatus()) { - case INFO: - case LOG: - case SUCCESS: - log.info(logMessage); - break; - case ERROR: - log.error(logMessage); - break; - case WARNING: - log.warn(logMessage); - break; + if (logBehaviour.equals(LogBehaviour.WRITE_TO_LOG)) { + switch (code.getStatus()) { + case INFO: + case LOG: + case SUCCESS: + log.info(logMessage); + break; + case ERROR: + log.error(logMessage); + break; + case WARNING: + log.warn(logMessage); + break; + } } JsonObject j = DefaultMessages.notification(code, message, params); diff --git a/doc/modbustcp-api.md b/doc/modbustcp-api.md index c4a5220a46d..7bac6e48c05 100644 --- a/doc/modbustcp-api.md +++ b/doc/modbustcp-api.md @@ -1,5 +1,7 @@ # OpenEMS Modbus/TCP-Api +This describes the OpenEMS Modbus/TCP client implementation. For a working example see [openems-modbus-master](https://github.com/OpenEMS/openems-modbus-master). + ## Setup Modbus/TCP-Controller OpenEMS Modbus/TCP-Api is implemented as a Controller. To be active it needs to be activated in the Scheduler. Default port is 502. @@ -32,4 +34,4 @@ System.out.println(r[0]); System.out.println(r[1]); ``` -Note: Only modbus function code 04 "Read Input Registers" is implemented for now, so there is no write functionality available yet. \ No newline at end of file +Note: Only modbus function code 04 "Read Input Registers" is implemented for now, so there is no write functionality available yet. diff --git a/edge/resources/logback.xml b/edge/resources/logback.xml index e1fb0adf531..49c19fbf06d 100644 --- a/edge/resources/logback.xml +++ b/edge/resources/logback.xml @@ -1,44 +1,46 @@ - - - - - - - [%-8.8thread] [%-5level] [%-20.20logger{36}:%-3line] %msg%ex{10}%n - - - - - - - - /var/log/openems/openems.%d{yyyy-MM-dd}.log - - - 7 - - - - [%-8.8thread] [%-5level] [%-20.20logger{36}:%-3line] %msg%ex{10}%n - - - - - - - - - - + + + + [%-8.8thread] [%-5level] [%-20.20logger{36}:%-3line]%msg%ex{10}%n + + + + + + /var/log/openems.log + + /var/log/openems.%i.log.zip + + 1 + 3 + 10MB + + + 5KB + + + [%-8.8thread] [%-5level] [%-20.20logger{36}:%-3line]%msg%ex{10}%n + + + + + + + + diff --git a/edge/src/io/openems/api/channel/ConfigChannel.java b/edge/src/io/openems/api/channel/ConfigChannel.java index e7e9f4cd371..e537e2756ea 100644 --- a/edge/src/io/openems/api/channel/ConfigChannel.java +++ b/edge/src/io/openems/api/channel/ConfigChannel.java @@ -37,7 +37,7 @@ public class ConfigChannel extends WriteChannel { private Optional defaultValue = Optional.empty(); - private boolean isOptional; + private Optional isOptional = Optional.empty(); // Empty defaults to false public ConfigChannel(String id, Thing parent) { super(id, parent); @@ -54,8 +54,10 @@ public ConfigChannel(String id, Thing parent) { @Override public void applyChannelDoc(ChannelDoc channelDoc) throws OpenemsException { super.applyChannelDoc(channelDoc); - this.isOptional = channelDoc.isOptional(); - if (!channelDoc.getDefaultValue().isEmpty()) { + if (!this.isOptional.isPresent()) { + this.isOptional = Optional.of(channelDoc.isOptional()); + } + if (!this.defaultValue.isPresent() && !channelDoc.getDefaultValue().isEmpty()) { JsonElement jValue = null; try { jValue = (new JsonParser()).parse(channelDoc.getDefaultValue()); @@ -99,7 +101,7 @@ public Optional getDefaultValue() { } public boolean isOptional() { - return isOptional; + return isOptional.orElse(false); } @Override diff --git a/edge/src/io/openems/api/channel/ReadChannel.java b/edge/src/io/openems/api/channel/ReadChannel.java index 84c8fe98590..d72a9b3a645 100644 --- a/edge/src/io/openems/api/channel/ReadChannel.java +++ b/edge/src/io/openems/api/channel/ReadChannel.java @@ -52,6 +52,7 @@ public class ReadChannel implements Channel, Comparable> { private Optional> type = Optional.empty(); protected Optional delta = Optional.empty(); + private Optional ignore = Optional.empty(); protected TreeMap labels = new TreeMap(); protected Optional multiplier = Optional.empty(); protected boolean negate = false; @@ -178,6 +179,17 @@ public ReadChannel delta(Long delta) { return this; } + /** + * Sets the ignore value. If the value of this channel is being updated to this value, it is getting ignored. + * + * @param ignore + * @return + */ + public ReadChannel ignore(T ignore) { + this.ignore = Optional.ofNullable(ignore); + return this; + } + public Optional deltaOptional() { return delta; } @@ -227,10 +239,10 @@ protected void updateValue(T value) { */ protected void updateValue(T newValue, boolean triggerEvent) { Optional oldValue = this.value; - if (newValue == null) { + if (newValue == null || (this.ignore.isPresent() && this.ignore.get().equals(newValue))) { this.value = Optional.empty(); - } - if (newValue instanceof Number && (multiplier.isPresent() || delta.isPresent() || negate)) { + + } else if (newValue instanceof Number && (multiplier.isPresent() || delta.isPresent() || negate)) { // special treatment for Numbers with given multiplier or delta Number number = (Number) newValue; double multiplier = 1; @@ -247,6 +259,7 @@ protected void updateValue(T newValue, boolean triggerEvent) { number = (long) (number.longValue() * multiplier - delta); @SuppressWarnings("unchecked") Optional value = (Optional) Optional.of(number); this.value = value; + } else { this.value = Optional.ofNullable(newValue); } diff --git a/edge/src/io/openems/api/device/nature/charger/ChargerNature.java b/edge/src/io/openems/api/device/nature/charger/ChargerNature.java index f32727f2cd8..44af3631ac3 100644 --- a/edge/src/io/openems/api/device/nature/charger/ChargerNature.java +++ b/edge/src/io/openems/api/device/nature/charger/ChargerNature.java @@ -33,12 +33,16 @@ public interface ChargerNature extends DeviceNature { @ChannelInfo(title = "maxActualPower", description = "Holds the maximum ever actual power.", type = Long.class, defaultValue = "0") public ConfigChannel maxActualPower(); + @ChannelInfo(type = Long.class) public WriteChannel setMaxPower(); + @ChannelInfo(type = Long.class) public ReadChannel getActualPower(); + @ChannelInfo(type = Long.class) public ReadChannel getNominalPower(); + @ChannelInfo(type = Long.class) public ReadChannel getInputVoltage(); public default void updateMaxChargerActualPower() { diff --git a/edge/src/io/openems/api/device/nature/ess/AsymmetricEssNature.java b/edge/src/io/openems/api/device/nature/ess/AsymmetricEssNature.java index 9d9a4fb98ee..35af71fad48 100644 --- a/edge/src/io/openems/api/device/nature/ess/AsymmetricEssNature.java +++ b/edge/src/io/openems/api/device/nature/ess/AsymmetricEssNature.java @@ -22,35 +22,48 @@ import io.openems.api.channel.ReadChannel; import io.openems.api.channel.WriteChannel; +import io.openems.api.doc.ChannelInfo; public interface AsymmetricEssNature extends EssNature { /* * ReadChannels */ + @ChannelInfo(type = Long.class) public ReadChannel activePowerL1(); + @ChannelInfo(type = Long.class) public ReadChannel activePowerL2(); + @ChannelInfo(type = Long.class) public ReadChannel activePowerL3(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePowerL1(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePowerL2(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePowerL3(); /* * WriteChannels */ + @ChannelInfo(type = Long.class) public WriteChannel setActivePowerL1(); + @ChannelInfo(type = Long.class) public WriteChannel setActivePowerL2(); + @ChannelInfo(type = Long.class) public WriteChannel setActivePowerL3(); + @ChannelInfo(type = Long.class) public WriteChannel setReactivePowerL1(); + @ChannelInfo(type = Long.class) public WriteChannel setReactivePowerL2(); + @ChannelInfo(type = Long.class) public WriteChannel setReactivePowerL3(); } diff --git a/edge/src/io/openems/api/device/nature/ess/EssNature.java b/edge/src/io/openems/api/device/nature/ess/EssNature.java index 96c7c504b9b..b735451c74f 100644 --- a/edge/src/io/openems/api/device/nature/ess/EssNature.java +++ b/edge/src/io/openems/api/device/nature/ess/EssNature.java @@ -61,16 +61,22 @@ public interface EssNature extends DeviceNature { @ChannelInfo(type = Long.class) public ReadChannel soc(); + @ChannelInfo(type = Long.class) public ReadChannel systemState(); + @ChannelInfo(type = Long.class) public ReadChannel allowedCharge(); + @ChannelInfo(type = Long.class) public ReadChannel allowedDischarge(); + @ChannelInfo(type = Long.class) public ReadChannel allowedApparent(); + @ChannelInfo(type = Long.class) public ReadChannel capacity(); + @ChannelInfo(type = Long.class) public ReadChannel maxNominalPower(); public StatusBitChannels warning(); diff --git a/edge/src/io/openems/api/device/nature/ess/SymmetricEssNature.java b/edge/src/io/openems/api/device/nature/ess/SymmetricEssNature.java index 3f586e6771d..58648211b8a 100644 --- a/edge/src/io/openems/api/device/nature/ess/SymmetricEssNature.java +++ b/edge/src/io/openems/api/device/nature/ess/SymmetricEssNature.java @@ -31,15 +31,19 @@ public interface SymmetricEssNature extends EssNature { @ChannelInfo(type = Long.class) public ReadChannel activePower(); + @ChannelInfo(type = Long.class) public ReadChannel apparentPower(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePower(); /* * WriteChannels */ + @ChannelInfo(type = Long.class) public WriteChannel setActivePower(); + @ChannelInfo(type = Long.class) public WriteChannel setReactivePower(); } diff --git a/edge/src/io/openems/api/device/nature/meter/AsymmetricMeterNature.java b/edge/src/io/openems/api/device/nature/meter/AsymmetricMeterNature.java index b7d25d7fbcc..b866d453293 100644 --- a/edge/src/io/openems/api/device/nature/meter/AsymmetricMeterNature.java +++ b/edge/src/io/openems/api/device/nature/meter/AsymmetricMeterNature.java @@ -31,28 +31,40 @@ public interface AsymmetricMeterNature extends MeterNature { /* * ReadChannels */ + @ChannelInfo(type = Long.class) public ReadChannel activePowerL1(); + @ChannelInfo(type = Long.class) public ReadChannel activePowerL2(); + @ChannelInfo(type = Long.class) public ReadChannel activePowerL3(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePowerL1(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePowerL2(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePowerL3(); + @ChannelInfo(type = Long.class) public ReadChannel currentL1(); + @ChannelInfo(type = Long.class) public ReadChannel currentL2(); + @ChannelInfo(type = Long.class) public ReadChannel currentL3(); + @ChannelInfo(type = Long.class) public ReadChannel voltageL1(); + @ChannelInfo(type = Long.class) public ReadChannel voltageL2(); + @ChannelInfo(type = Long.class) public ReadChannel voltageL3(); @ChannelInfo(title = "maxActivePower", description = "Holds the maximum ever active power.", type = Long.class, defaultValue = "0") diff --git a/edge/src/io/openems/api/device/nature/meter/SymmetricMeterNature.java b/edge/src/io/openems/api/device/nature/meter/SymmetricMeterNature.java index 05b5c9e65bf..ed6b53d2099 100644 --- a/edge/src/io/openems/api/device/nature/meter/SymmetricMeterNature.java +++ b/edge/src/io/openems/api/device/nature/meter/SymmetricMeterNature.java @@ -31,7 +31,7 @@ public interface SymmetricMeterNature extends MeterNature { /* * ReadChannels */ - + @ChannelInfo(type = Long.class) public ReadChannel activePower(); @ChannelInfo(title = "maxActivePower", description = "Holds the maximum ever active power.", type = Long.class, defaultValue = "0") @@ -40,12 +40,16 @@ public interface SymmetricMeterNature extends MeterNature { @ChannelInfo(title = "minActivePower", description = "Holds the minimum ever active power.", type = Long.class, defaultValue = "0") public ConfigChannel minActivePower(); + @ChannelInfo(type = Long.class) public ReadChannel apparentPower(); + @ChannelInfo(type = Long.class) public ReadChannel reactivePower(); + @ChannelInfo(type = Long.class) public ReadChannel frequency(); + @ChannelInfo(type = Long.class) public ReadChannel voltage(); public default void updateMinMaxSymmetricActivePower() { diff --git a/edge/src/io/openems/core/Config.java b/edge/src/io/openems/core/Config.java index 0474d96ff49..30b46f3bdcc 100644 --- a/edge/src/io/openems/core/Config.java +++ b/edge/src/io/openems/core/Config.java @@ -222,6 +222,7 @@ private JsonObject addDefaultConfig(JsonObject jConfig) { * @throws NotImplementedException */ public void writeConfigFile() throws NotImplementedException { + // TODO send config to all attached websockets // get config as json JsonObject jConfig = getJson(ConfigFormat.FILE); diff --git a/edge/src/io/openems/core/utilities/BitUtils.java b/edge/src/io/openems/core/utilities/BitUtils.java index 9774ee64647..6fc4debf53e 100644 --- a/edge/src/io/openems/core/utilities/BitUtils.java +++ b/edge/src/io/openems/core/utilities/BitUtils.java @@ -12,7 +12,7 @@ public class BitUtils { private final static int BYTES_LONG = 8; private final static int BITS_BOOLEAN = 1; - public final static ByteOrder BYTE_ODER = ByteOrder.BIG_ENDIAN; + public final static ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN; public static int getBitLength(Class type) throws NotImplementedException { switch (OpenemsTypes.get(type)) { @@ -43,10 +43,10 @@ public static byte[] toBytes(Object value) throws NotImplementedException { Class type = value.getClass(); switch (OpenemsTypes.get(type)) { case INTEGER: - return ByteBuffer.allocate(BYTES_INT).order(BYTE_ODER).putInt((Integer) value).array(); + return ByteBuffer.allocate(BYTES_INT).order(BYTE_ORDER).putInt((Integer) value).array(); case LONG: - return ByteBuffer.allocate(BYTES_LONG).order(BYTE_ODER).putLong((Long) value).array(); + return ByteBuffer.allocate(BYTES_LONG).order(BYTE_ORDER).putLong((Long) value).array(); case BOOLEAN: // TODO put boolean value in a byte case DOUBLE: // TODO diff --git a/edge/src/io/openems/core/utilities/websocket/EdgeWebsocketHandler.java b/edge/src/io/openems/core/utilities/websocket/EdgeWebsocketHandler.java index c84ac72e2b1..3295d478f1f 100644 --- a/edge/src/io/openems/core/utilities/websocket/EdgeWebsocketHandler.java +++ b/edge/src/io/openems/core/utilities/websocket/EdgeWebsocketHandler.java @@ -45,6 +45,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.utils.JsonUtils; import io.openems.common.websocket.DefaultMessages; +import io.openems.common.websocket.LogBehaviour; import io.openems.common.websocket.Notification; import io.openems.common.websocket.WebSocketUtils; import io.openems.core.Config; @@ -206,8 +207,8 @@ private synchronized JsonObject config(JsonObject jConfig) { ConfigChannel configChannel = (ConfigChannel) channel; Object value = ConfigUtils.getConfigObject(configChannel, jValue); configChannel.updateValue(value, true); - WebSocketUtils.sendNotification(websocketOpt, Notification.EDGE_CHANNEL_UPDATE_SUCCESS, - channel.address() + " => " + jValue); + WebSocketUtils.sendNotification(websocketOpt, LogBehaviour.WRITE_TO_LOG, + Notification.EDGE_CHANNEL_UPDATE_SUCCESS, channel.address() + " => " + jValue); } else if (channel instanceof WriteChannel) { /* diff --git a/edge/src/io/openems/impl/controller/api/modbustcp/ModbusTcpApiController.java b/edge/src/io/openems/impl/controller/api/modbustcp/ModbusTcpApiController.java index 97c9aff1dd5..1c21d757b7f 100644 --- a/edge/src/io/openems/impl/controller/api/modbustcp/ModbusTcpApiController.java +++ b/edge/src/io/openems/impl/controller/api/modbustcp/ModbusTcpApiController.java @@ -9,6 +9,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import io.openems.api.channel.Channel; import io.openems.api.channel.ConfigChannel; import io.openems.api.controller.Controller; import io.openems.api.doc.ChannelDoc; @@ -111,7 +112,13 @@ protected void updateChannelMapping(Optional jMappingOpt) { if (channelDocOpt.isPresent()) { processImage.addMapping(ref, channelAddress, channelDocOpt.get()); } else { - throw new OpenemsException("ChannelDoc for channel [" + channelAddress + "] is not available."); + Optional channelOpt = thingRepository.getChannel(channelAddress); + if (channelOpt.isPresent()) { + throw new OpenemsException( + "ChannelDoc for channel [" + channelAddress + "] is not available."); + } else { + throw new OpenemsException("Channel [" + channelAddress + "] does not exist."); + } } } catch (Exception e) { log.error("Unable to add channel mapping: " + e.getMessage()); diff --git a/edge/src/io/openems/impl/controller/api/modbustcp/MyProcessImage.java b/edge/src/io/openems/impl/controller/api/modbustcp/MyProcessImage.java index 62456a1a4c6..4dcfa3e4f06 100644 --- a/edge/src/io/openems/impl/controller/api/modbustcp/MyProcessImage.java +++ b/edge/src/io/openems/impl/controller/api/modbustcp/MyProcessImage.java @@ -179,6 +179,7 @@ public synchronized InputRegister[] getInputRegisterRange(int offset, int count) int ref = i + offset; // get channel value as InputRegister[] if (!refs.containsKey(ref)) { + this.throwIllegalAddressException("No mapping defined for Modbus address [" + ref + "]."); } ChannelAddress channelAddress = refs.get(ref); diff --git a/edge/src/io/openems/impl/controller/api/websocket/WebsocketApiServer.java b/edge/src/io/openems/impl/controller/api/websocket/WebsocketApiServer.java index 4431995a3c8..fec71969ee7 100644 --- a/edge/src/io/openems/impl/controller/api/websocket/WebsocketApiServer.java +++ b/edge/src/io/openems/impl/controller/api/websocket/WebsocketApiServer.java @@ -37,6 +37,7 @@ import io.openems.common.utils.JsonUtils; import io.openems.common.websocket.AbstractWebsocketServer; import io.openems.common.websocket.DefaultMessages; +import io.openems.common.websocket.LogBehaviour; import io.openems.common.websocket.Notification; import io.openems.common.websocket.WebSocketUtils; import io.openems.impl.controller.api.websocket.session.WebsocketApiSession; @@ -73,7 +74,7 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { // refresh session session.getData().getWebsocketHandler().setWebsocket(websocket); // add to websockets - this.websockets.forcePut(websocket, session); + this.addWebsocket(websocket, session); // send connection successful to browser JsonObject jReply = DefaultMessages.browserConnectionSuccessfulReply(session.getToken(), Optional.of(session.getData().getRole()), new ArrayList<>()); @@ -83,8 +84,8 @@ protected void _onOpen(WebSocket websocket, ClientHandshake handshake) { return; } // if we are here, automatic authentication was not possible -> notify client - WebSocketUtils.sendNotification(websocket, Notification.EDGE_AUTHENTICATION_BY_TOKEN_FAILED, - tokenOpt.orElse("")); + WebSocketUtils.sendNotification(websocket, LogBehaviour.WRITE_TO_LOG, + Notification.EDGE_AUTHENTICATION_BY_TOKEN_FAILED, tokenOpt.orElse("")); } } @@ -103,7 +104,7 @@ protected void _onMessage(WebSocket websocket, JsonObject jMessage, Optional()); @@ -169,7 +170,7 @@ private Optional authenticate(JsonElement jAuthenticateElem /* * Logout and close session */ - this.websockets.remove(websocket); + this.removeWebsocket(websocket); } } } catch (OpenemsException e) { /* ignore */ } diff --git a/edge/src/io/openems/impl/controller/riedmann/RiedmannController.java b/edge/src/io/openems/impl/controller/riedmann/RiedmannController.java index a4c7fb7bf22..417066cbc09 100644 --- a/edge/src/io/openems/impl/controller/riedmann/RiedmannController.java +++ b/edge/src/io/openems/impl/controller/riedmann/RiedmannController.java @@ -1,249 +1,229 @@ -package io.openems.impl.controller.riedmann; - -import java.util.Optional; - -import io.openems.api.channel.Channel; -import io.openems.api.channel.ChannelChangeListener; -import io.openems.api.channel.ConfigChannel; -import io.openems.api.controller.Controller; -import io.openems.api.device.nature.ess.EssNature; -import io.openems.api.doc.ChannelInfo; -import io.openems.api.doc.ThingInfo; -import io.openems.api.exception.InvalidValueException; -import io.openems.api.exception.WriteChannelException; - -@ThingInfo(title = "Test write") -public class RiedmannController extends Controller implements ChannelChangeListener { - - /* - * Config-Channel - */ - @ChannelInfo(title = "System Stop", description = "This configuration stops the system.", type = Boolean.class) - public ConfigChannel signalSystemStop = new ConfigChannel("signalSystemStop", this) - .defaultValue(true); - @ChannelInfo(title = "Waterlevel Borehole 1 On", description = "This configuration sets the waterlevel to start Borehole Pump 1", type = Long.class) - public ConfigChannel setWaterLevelBorehole1On = new ConfigChannel("wl1On", this).defaultValue(50L); - @ChannelInfo(title = "Waterlevel Borehole 1 Off", description = "This configuration sets the waterlevel to stop Borehole Pump 1", type = Long.class) - public ConfigChannel setWaterLevelBorehole1Off = new ConfigChannel("wl1Off", this).defaultValue(100L); - @ChannelInfo(title = "Waterlevel Borehole 2 On", description = "This configuration sets the waterlevel to start Borehole Pump 2", type = Long.class) - public ConfigChannel setWaterLevelBorehole2On = new ConfigChannel("wl2On", this).defaultValue(200L); - @ChannelInfo(title = "Waterlevel Borehole 2 Off", description = "This configuration sets the waterlevel to stop Borehole Pump 2", type = Long.class) - public ConfigChannel setWaterLevelBorehole2Off = new ConfigChannel("wl2Off", this).defaultValue(300L); - @ChannelInfo(title = "Waterlevel Borehole 3 On", description = "This configuration sets the waterlevel to start Borehole Pump 3", type = Long.class) - public ConfigChannel setWaterLevelBorehole3On = new ConfigChannel("wl3On", this).defaultValue(400L); - @ChannelInfo(title = "Waterlevel Borehole 3 Off", description = "This configuration sets the waterlevel to stop Borehole Pump 3", type = Long.class) - public ConfigChannel setWaterLevelBorehole3Off = new ConfigChannel("wl3Off", this).defaultValue(500L); - @ChannelInfo(title = "Soc Hysteresis", description = "hysteresis for the switching of the loads.", type = Long.class) - public ConfigChannel socHysteresis = new ConfigChannel("socHysteresis", this).defaultValue(10L); - @ChannelInfo(title = "Soc Load 1 Off", description = "Below this Soc the Load 1 will be disconnected.", type = Long.class) - public ConfigChannel socLoad1Off = new ConfigChannel<>("socLoad1Off", this); - @ChannelInfo(title = "Soc Load 2 Off", description = "Below this Soc the Load 2 will be disconnected.", type = Long.class) - public ConfigChannel socLoad2Off = new ConfigChannel<>("socLoad2Off", this); - @ChannelInfo(title = "Soc Load 3 Off", description = "Below this Soc the Load 3 will be disconnected.", type = Long.class) - public ConfigChannel socLoad3Off = new ConfigChannel<>("socLoad3Off", this); - @ChannelInfo(title = "Soc Load 4 Off", description = "Below this Soc the Load 4 will be disconnected.", type = Long.class) - public ConfigChannel socLoad4Off = new ConfigChannel<>("socLoad4Off", this); - - @ChannelInfo(title = "SPS", description = "The sps which should be controlled.", type = Custom.class) - public ConfigChannel sps = new ConfigChannel<>("sps", this); - @ChannelInfo(title = "ESS", description = "The ess to stop on system stop. Also used for Off-Grid indication for the SPS. ", type = Ess.class) - public ConfigChannel ess = new ConfigChannel<>("ess", this); - - /* - * Attributes - */ - private boolean watchdogState = false; - private boolean updateWaterLevelBorehole1On = false; - private boolean updateWaterLevelBorehole1Off = false; - private boolean updateWaterLevelBorehole2On = false; - private boolean updateWaterLevelBorehole2Off = false; - private boolean updateWaterLevelBorehole3On = false; - private boolean updateWaterLevelBorehole3Off = false; - private boolean load1On = true; - private boolean load2On = true; - private boolean load3On = true; - private boolean load4On = true; - - public RiedmannController() { - super(); - } - - public RiedmannController(String thingId) { - super(thingId); - } - - @Override - public void run() { - try { - Ess ess = this.ess.value(); - Custom sps = this.sps.value(); - // Grid-Mode - try { - if (ess.gridMode.labelOptional().equals(Optional.of(EssNature.OFF_GRID))) { - sps.signalGridOn.pushWrite(0L); - } else { - sps.signalGridOn.pushWrite(1L); - } - } catch (WriteChannelException e) { - log.error("Failed to set off-Grid indication to sps.", e); - } - // Stop - try { - if (signalSystemStop.value()) { - ess.setWorkState.pushWriteFromLabel(EssNature.STOP); - sps.signalSystemStop.pushWrite(1L); - } else { - sps.signalSystemStop.pushWrite(0L); - } - } catch (WriteChannelException e) { - log.error("Failed to set system stop!", e); - } - // Watchdog - try { - if (watchdogState) { - sps.watchdog.pushWrite(0L); - watchdogState = false; - } else { - sps.watchdog.pushWrite(1L); - watchdogState = true; - } - } catch (WriteChannelException e) { - log.error("Failed to set Watchdog!", e); - } - // Water level - if (updateWaterLevelBorehole1Off) { - try { - sps.setWaterLevelBorehole1Off.pushWrite(setWaterLevelBorehole1Off.value()); - updateWaterLevelBorehole1Off = false; - } catch (InvalidValueException | WriteChannelException e) { - log.error("Failed to set WaterLevelBorehole1Off!", e); - } - } - if (updateWaterLevelBorehole1On) { - try { - sps.setWaterLevelBorehole1On.pushWrite(setWaterLevelBorehole1On.value()); - updateWaterLevelBorehole1On = false; - } catch (InvalidValueException | WriteChannelException e) { - log.error("Failed to set WaterLevelBorehole1On!", e); - } - } - if (updateWaterLevelBorehole2Off) { - try { - sps.setWaterLevelBorehole2Off.pushWrite(setWaterLevelBorehole2Off.value()); - updateWaterLevelBorehole2Off = false; - } catch (InvalidValueException | WriteChannelException e) { - log.error("Failed to set WaterLevelBorehole2Off!", e); - } - } - if (updateWaterLevelBorehole2On) { - try { - sps.setWaterLevelBorehole2On.pushWrite(setWaterLevelBorehole2On.value()); - updateWaterLevelBorehole2On = false; - } catch (InvalidValueException | WriteChannelException e) { - log.error("Failed to set WaterLevelBorehole2On!", e); - } - } - if (updateWaterLevelBorehole3Off) { - try { - sps.setWaterLevelBorehole3Off.pushWrite(setWaterLevelBorehole3Off.value()); - updateWaterLevelBorehole3Off = false; - } catch (InvalidValueException | WriteChannelException e) { - log.error("Failed to set WaterLevelBorehole3Off!", e); - } - } - if (updateWaterLevelBorehole3On) { - try { - sps.setWaterLevelBorehole3On.pushWrite(setWaterLevelBorehole3On.value()); - updateWaterLevelBorehole3On = false; - } catch (InvalidValueException | WriteChannelException e) { - log.error("Failed to set WaterLevelBorehole3On!", e); - } - } - // Load switching - try { - if (ess.soc.value() >= socLoad1Off.value() + socHysteresis.value()) { - load1On = true; - } else if (ess.soc.value() <= socLoad1Off.value()) { - load1On = false; - } - if (load1On) { - sps.setClima1On.pushWrite(1L); - sps.setClima2On.pushWrite(1L); - } else { - sps.setClima1On.pushWrite(0L); - sps.setClima2On.pushWrite(0L); - } - } catch (WriteChannelException e) { - log.error("Failed to connect/disconnect Load 1", e); - } - try { - if (ess.soc.value() >= socLoad2Off.value() + socHysteresis.value()) { - load2On = true; - } else if (ess.soc.value() <= socLoad2Off.value()) { - load2On = false; - } - if (load2On) { - sps.setPivotOn.pushWrite(1L); - } else { - sps.setPivotOn.pushWrite(0L); - } - } catch (WriteChannelException e) { - log.error("Failed to connect/disconnect Load 2", e); - } - try { - if (ess.soc.value() >= socLoad3Off.value() + socHysteresis.value()) { - load3On = true; - } else if (ess.soc.value() <= socLoad3Off.value()) { - load3On = false; - } - if (load3On) { - sps.setBorehole1On.pushWrite(1L); - sps.setBorehole2On.pushWrite(1L); - sps.setBorehole3On.pushWrite(1L); - } else { - sps.setBorehole1On.pushWrite(0L); - sps.setBorehole2On.pushWrite(0L); - sps.setBorehole3On.pushWrite(0L); - } - } catch (WriteChannelException e) { - log.error("Failed to connect/disconnect Load 3", e); - } - try { - if (ess.soc.value() >= socLoad4Off.value() + socHysteresis.value()) { - load4On = true; - } else if (ess.soc.value() <= socLoad4Off.value()) { - load4On = false; - } - if (load4On) { - sps.setOfficeOn.pushWrite(1L); - sps.setTraineeCenterOn.pushWrite(1L); - } else { - sps.setOfficeOn.pushWrite(0L); - sps.setTraineeCenterOn.pushWrite(0L); - } - } catch (WriteChannelException e) { - log.error("Failed to connect/disconnect Load 4", e); - } - } catch (InvalidValueException e) { - log.error("Can't read value!", e); - } - } - - @Override - public void channelChanged(Channel channel, Optional newValue, Optional oldValue) { - if (channel.equals(setWaterLevelBorehole1Off)) { - updateWaterLevelBorehole1Off = true; - } else if (channel.equals(setWaterLevelBorehole1On)) { - updateWaterLevelBorehole1On = true; - } else if (channel.equals(setWaterLevelBorehole2Off)) { - updateWaterLevelBorehole2Off = true; - } else if (channel.equals(setWaterLevelBorehole2On)) { - updateWaterLevelBorehole2On = true; - } else if (channel.equals(setWaterLevelBorehole3Off)) { - updateWaterLevelBorehole3Off = true; - } else if (channel.equals(setWaterLevelBorehole3On)) { - updateWaterLevelBorehole3On = true; - } - } - -} +package io.openems.impl.controller.riedmann; + +import java.util.Optional; + +import io.openems.api.channel.Channel; +import io.openems.api.channel.ChannelChangeListener; +import io.openems.api.channel.ConfigChannel; +import io.openems.api.controller.Controller; +import io.openems.api.device.nature.ess.EssNature; +import io.openems.api.doc.ChannelInfo; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.InvalidValueException; +import io.openems.api.exception.WriteChannelException; + +@ThingInfo(title = "Sps parameter Controller") +public class RiedmannController extends Controller implements ChannelChangeListener { + + /* + * Config-Channel + */ + + @ChannelInfo(title = "Waterlevel Borehole 1 On", description = "This configuration sets the waterlevel to start Borehole Pump 1", type = Long.class) + public ConfigChannel setWaterLevelBorehole1On = new ConfigChannel("wl1On", this).defaultValue(50L); + @ChannelInfo(title = "Waterlevel Borehole 1 Off", description = "This configuration sets the waterlevel to stop Borehole Pump 1", type = Long.class) + public ConfigChannel setWaterLevelBorehole1Off = new ConfigChannel("wl1Off", this).defaultValue(100L); + @ChannelInfo(title = "Waterlevel Borehole 2 On", description = "This configuration sets the waterlevel to start Borehole Pump 2", type = Long.class) + public ConfigChannel setWaterLevelBorehole2On = new ConfigChannel("wl2On", this).defaultValue(200L); + @ChannelInfo(title = "Waterlevel Borehole 2 Off", description = "This configuration sets the waterlevel to stop Borehole Pump 2", type = Long.class) + public ConfigChannel setWaterLevelBorehole2Off = new ConfigChannel("wl2Off", this).defaultValue(300L); + @ChannelInfo(title = "Waterlevel Borehole 3 On", description = "This configuration sets the waterlevel to start Borehole Pump 3", type = Long.class) + public ConfigChannel setWaterLevelBorehole3On = new ConfigChannel("wl3On", this).defaultValue(400L); + @ChannelInfo(title = "Waterlevel Borehole 3 Off", description = "This configuration sets the waterlevel to stop Borehole Pump 3", type = Long.class) + public ConfigChannel setWaterLevelBorehole3Off = new ConfigChannel("wl3Off", this).defaultValue(500L); + @ChannelInfo(title = "Soc Hysteresis", description = "hysteresis for the switching of the loads.", type = Long.class) + public ConfigChannel socHysteresis = new ConfigChannel("socHysteresis", this).defaultValue(10L); + @ChannelInfo(title = "Soc Load 1 Off", description = "Below this Soc the Load 1 will be disconnected.", type = Long.class) + public ConfigChannel socLoad1Off = new ConfigChannel<>("socLoad1Off", this); + @ChannelInfo(title = "Soc Load 2 Off", description = "Below this Soc the Load 2 will be disconnected.", type = Long.class) + public ConfigChannel socLoad2Off = new ConfigChannel<>("socLoad2Off", this); + @ChannelInfo(title = "Soc Load 3 Off", description = "Below this Soc the Load 3 will be disconnected.", type = Long.class) + public ConfigChannel socLoad3Off = new ConfigChannel<>("socLoad3Off", this); + @ChannelInfo(title = "Soc Load 4 Off", description = "Below this Soc the Load 4 will be disconnected.", type = Long.class) + public ConfigChannel socLoad4Off = new ConfigChannel<>("socLoad4Off", this); + + @ChannelInfo(title = "SPS", description = "The sps which should be controlled.", type = Custom.class) + public ConfigChannel sps = new ConfigChannel<>("sps", this); + @ChannelInfo(title = "ESS", description = "The ess to stop on system stop. Also used for Off-Grid indication for the SPS. ", type = Ess.class) + public ConfigChannel ess = new ConfigChannel<>("ess", this); + + /* + * Attributes + */ + private boolean watchdogState = false; + private boolean updateWaterLevelBorehole1On = false; + private boolean updateWaterLevelBorehole1Off = false; + private boolean updateWaterLevelBorehole2On = false; + private boolean updateWaterLevelBorehole2Off = false; + private boolean updateWaterLevelBorehole3On = false; + private boolean updateWaterLevelBorehole3Off = false; + private boolean load1On = true; + private boolean load2On = true; + private boolean load3On = true; + private boolean load4On = true; + + public RiedmannController() { + super(); + } + + public RiedmannController(String thingId) { + super(thingId); + } + + @Override + public void run() { + try { + Ess ess = this.ess.value(); + Custom sps = this.sps.value(); + // Watchdog + try { + if (watchdogState) { + sps.watchdog.pushWrite(0L); + watchdogState = false; + } else { + sps.watchdog.pushWrite(1L); + watchdogState = true; + } + } catch (WriteChannelException e) { + log.error("Failed to set Watchdog!", e); + } + // Water level + if (updateWaterLevelBorehole1Off) { + try { + sps.setWaterLevelBorehole1Off.pushWrite(setWaterLevelBorehole1Off.value()); + updateWaterLevelBorehole1Off = false; + } catch (InvalidValueException | WriteChannelException e) { + log.error("Failed to set WaterLevelBorehole1Off!", e); + } + } + if (updateWaterLevelBorehole1On) { + try { + sps.setWaterLevelBorehole1On.pushWrite(setWaterLevelBorehole1On.value()); + updateWaterLevelBorehole1On = false; + } catch (InvalidValueException | WriteChannelException e) { + log.error("Failed to set WaterLevelBorehole1On!", e); + } + } + if (updateWaterLevelBorehole2Off) { + try { + sps.setWaterLevelBorehole2Off.pushWrite(setWaterLevelBorehole2Off.value()); + updateWaterLevelBorehole2Off = false; + } catch (InvalidValueException | WriteChannelException e) { + log.error("Failed to set WaterLevelBorehole2Off!", e); + } + } + if (updateWaterLevelBorehole2On) { + try { + sps.setWaterLevelBorehole2On.pushWrite(setWaterLevelBorehole2On.value()); + updateWaterLevelBorehole2On = false; + } catch (InvalidValueException | WriteChannelException e) { + log.error("Failed to set WaterLevelBorehole2On!", e); + } + } + if (updateWaterLevelBorehole3Off) { + try { + sps.setWaterLevelBorehole3Off.pushWrite(setWaterLevelBorehole3Off.value()); + updateWaterLevelBorehole3Off = false; + } catch (InvalidValueException | WriteChannelException e) { + log.error("Failed to set WaterLevelBorehole3Off!", e); + } + } + if (updateWaterLevelBorehole3On) { + try { + sps.setWaterLevelBorehole3On.pushWrite(setWaterLevelBorehole3On.value()); + updateWaterLevelBorehole3On = false; + } catch (InvalidValueException | WriteChannelException e) { + log.error("Failed to set WaterLevelBorehole3On!", e); + } + } + // Load switching + try { + if (ess.soc.value() >= socLoad1Off.value() + socHysteresis.value() + || ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + load1On = true; + } else if (ess.soc.value() <= socLoad1Off.value()) { + load1On = false; + } + if (load1On) { + sps.setClima1On.pushWrite(1L); + sps.setClima2On.pushWrite(1L); + } else { + sps.setClima1On.pushWrite(0L); + sps.setClima2On.pushWrite(0L); + } + } catch (WriteChannelException e) { + log.error("Failed to connect/disconnect Load 1", e); + } + try { + if (ess.soc.value() >= socLoad2Off.value() + socHysteresis.value() + || ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + load2On = true; + } else if (ess.soc.value() <= socLoad2Off.value()) { + load2On = false; + } + if (load2On) { + sps.setPivotOn.pushWrite(1L); + } else { + sps.setPivotOn.pushWrite(0L); + } + } catch (WriteChannelException e) { + log.error("Failed to connect/disconnect Load 2", e); + } + try { + if (ess.soc.value() >= socLoad3Off.value() + socHysteresis.value() + || ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + load3On = true; + } else if (ess.soc.value() <= socLoad3Off.value()) { + load3On = false; + } + if (load3On) { + sps.setBorehole1On.pushWrite(1L); + sps.setBorehole2On.pushWrite(1L); + sps.setBorehole3On.pushWrite(1L); + } else { + sps.setBorehole1On.pushWrite(0L); + sps.setBorehole2On.pushWrite(0L); + sps.setBorehole3On.pushWrite(0L); + } + } catch (WriteChannelException e) { + log.error("Failed to connect/disconnect Load 3", e); + } + try { + if (ess.soc.value() >= socLoad4Off.value() + socHysteresis.value() + || ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + load4On = true; + } else if (ess.soc.value() <= socLoad4Off.value()) { + load4On = false; + } + if (load4On) { + sps.setOfficeOn.pushWrite(1L); + sps.setTraineeCenterOn.pushWrite(1L); + } else { + sps.setOfficeOn.pushWrite(0L); + sps.setTraineeCenterOn.pushWrite(0L); + } + } catch (WriteChannelException e) { + log.error("Failed to connect/disconnect Load 4", e); + } + } catch (InvalidValueException e) { + log.error("Can't read value!", e); + } + } + + @Override + public void channelChanged(Channel channel, Optional newValue, Optional oldValue) { + if (channel.equals(setWaterLevelBorehole1Off)) { + updateWaterLevelBorehole1Off = true; + } else if (channel.equals(setWaterLevelBorehole1On)) { + updateWaterLevelBorehole1On = true; + } else if (channel.equals(setWaterLevelBorehole2Off)) { + updateWaterLevelBorehole2Off = true; + } else if (channel.equals(setWaterLevelBorehole2On)) { + updateWaterLevelBorehole2On = true; + } else if (channel.equals(setWaterLevelBorehole3Off)) { + updateWaterLevelBorehole3Off = true; + } else if (channel.equals(setWaterLevelBorehole3On)) { + updateWaterLevelBorehole3On = true; + } + } +} diff --git a/edge/src/io/openems/impl/controller/riedmann/SystemStopController.java b/edge/src/io/openems/impl/controller/riedmann/SystemStopController.java new file mode 100644 index 00000000000..e76fdf0e8a7 --- /dev/null +++ b/edge/src/io/openems/impl/controller/riedmann/SystemStopController.java @@ -0,0 +1,70 @@ +package io.openems.impl.controller.riedmann; + +import java.util.Optional; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.controller.Controller; +import io.openems.api.device.nature.ess.EssNature; +import io.openems.api.doc.ChannelInfo; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.InvalidValueException; +import io.openems.api.exception.WriteChannelException; + +@ThingInfo(title = "SystemStopController") +public class SystemStopController extends Controller { + + /* + * Config-Channel + */ + @ChannelInfo(title = "System Stop", description = "This configuration stops the system.", type = Boolean.class) + public ConfigChannel signalSystemStop = new ConfigChannel("signalSystemStop", this); + + @ChannelInfo(title = "SPS", description = "The sps which should be controlled.", type = Custom.class) + public ConfigChannel sps = new ConfigChannel<>("sps", this); + @ChannelInfo(title = "ESS", description = "The ess to stop on system stop. Also used for Off-Grid indication for the SPS. ", type = Ess.class) + public ConfigChannel ess = new ConfigChannel<>("ess", this); + + /* + * Attributes + */ + + public SystemStopController() { + super(); + } + + public SystemStopController(String thingId) { + super(thingId); + } + + @Override + public void run() { + try { + Ess ess = this.ess.value(); + Custom sps = this.sps.value(); + // Grid-Mode + try { + if (ess.gridMode.labelOptional().equals(Optional.of(EssNature.OFF_GRID))) { + sps.signalGridOn.pushWrite(0L); + } else { + sps.signalGridOn.pushWrite(1L); + } + } catch (WriteChannelException e) { + log.error("Failed to set off-Grid indication to sps.", e); + } + // Stop + try { + if (signalSystemStop.value()) { + ess.setWorkState.pushWriteFromLabel(EssNature.STOP); + sps.signalSystemStop.pushWrite(1L); + } else { + sps.signalSystemStop.pushWrite(0L); + } + } catch (WriteChannelException e) { + log.error("Failed to set system stop!", e); + } + } catch (InvalidValueException e) { + log.error("Can't read value!", e); + } + } + +} diff --git a/edge/src/io/openems/impl/controller/supplybusswitch/Ess.java b/edge/src/io/openems/impl/controller/supplybusswitch/Ess.java index 77fa17c0152..cfa7f4d2fac 100644 --- a/edge/src/io/openems/impl/controller/supplybusswitch/Ess.java +++ b/edge/src/io/openems/impl/controller/supplybusswitch/Ess.java @@ -1,91 +1,116 @@ -/******************************************************************************* - * OpenEMS - Open Source Energy Management System - * Copyright (c) 2016, 2017 FENECON GmbH and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Contributors: - * FENECON GmbH - initial API and implementation and initial documentation - *******************************************************************************/ -package io.openems.impl.controller.supplybusswitch; - -import java.util.Optional; - -import io.openems.api.channel.ReadChannel; -import io.openems.api.channel.WriteChannel; -import io.openems.api.controller.IsThingMap; -import io.openems.api.controller.ThingMap; -import io.openems.api.device.nature.ess.EssNature; -import io.openems.api.device.nature.ess.SymmetricEssNature; -import io.openems.api.exception.InvalidValueException; -import io.openems.api.exception.WriteChannelException; - -@IsThingMap(type = SymmetricEssNature.class) -public class Ess extends ThingMap { - - public final WriteChannel setActivePower; - public final WriteChannel setReactivePower; - public final ReadChannel soc; - public final ReadChannel activePower; - public final ReadChannel reactivePower; - public final ReadChannel allowedCharge; - public final ReadChannel allowedDischarge; - public final ReadChannel gridMode; - public final ReadChannel minSoc; - public final WriteChannel setWorkState; - public final ReadChannel systemState; - private Supplybus activeSupplybus; - - public Supplybus getActiveSupplybus() { - return activeSupplybus; - } - - public void setActiveSupplybus(Supplybus supplybus) { - this.activeSupplybus = supplybus; - } - - public Ess(SymmetricEssNature ess) { - super(ess); - setActivePower = ess.setActivePower().required(); - setReactivePower = ess.setReactivePower().required(); - reactivePower = ess.reactivePower(); - soc = ess.soc().required(); - minSoc = ess.minSoc().required(); - activePower = ess.activePower().required(); - allowedCharge = ess.allowedCharge().required(); - allowedDischarge = ess.allowedDischarge().required(); - gridMode = ess.gridMode().required(); - systemState = ess.systemState().required(); - setWorkState = ess.setWorkState().required(); - } - - public long useableSoc() throws InvalidValueException { - return soc.value() - minSoc.value(); - } - - public void start() throws WriteChannelException { - if (systemState.labelOptional().isPresent() && (systemState.labelOptional().equals(Optional.of(EssNature.STOP)) - || systemState.labelOptional().equals(Optional.of(EssNature.STANDBY)))) { - setWorkState.pushWriteFromLabel(EssNature.START); - } - } - - public void standby() throws WriteChannelException { - if (systemState.labelOptional().isPresent() - && systemState.labelOptional().equals(Optional.of(EssNature.START))) { - setWorkState.pushWriteFromLabel(EssNature.STANDBY); - } - } - -} +/******************************************************************************* + * OpenEMS - Open Source Energy Management System + * Copyright (c) 2016, 2017 FENECON GmbH and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Contributors: + * FENECON GmbH - initial API and implementation and initial documentation + *******************************************************************************/ +package io.openems.impl.controller.supplybusswitch; + +import io.openems.api.channel.ReadChannel; +import io.openems.api.channel.WriteChannel; +import io.openems.api.controller.IsThingMap; +import io.openems.api.controller.ThingMap; +import io.openems.api.device.nature.ess.EssNature; +import io.openems.api.device.nature.ess.SymmetricEssNature; +import io.openems.api.exception.InvalidValueException; +import io.openems.api.exception.WriteChannelException; + +@IsThingMap(type = SymmetricEssNature.class) +public class Ess extends ThingMap { + + public final WriteChannel setActivePower; + public final WriteChannel setReactivePower; + public final ReadChannel soc; + public final ReadChannel activePower; + public final ReadChannel reactivePower; + public final ReadChannel allowedCharge; + public final ReadChannel allowedDischarge; + public final ReadChannel gridMode; + public final ReadChannel minSoc; + public final WriteChannel setWorkState; + public final ReadChannel systemState; + private Supplybus activeSupplybus; + private WorkState currentState; + + enum WorkState { + START, STOP, STANDBY + } + + public Supplybus getActiveSupplybus() { + return activeSupplybus; + } + + public void setActiveSupplybus(Supplybus supplybus) { + this.activeSupplybus = supplybus; + } + + public Ess(SymmetricEssNature ess) { + super(ess); + setActivePower = ess.setActivePower().required(); + setReactivePower = ess.setReactivePower().required(); + reactivePower = ess.reactivePower(); + soc = ess.soc().required(); + minSoc = ess.minSoc().required(); + activePower = ess.activePower().required(); + allowedCharge = ess.allowedCharge().required(); + allowedDischarge = ess.allowedDischarge().required(); + gridMode = ess.gridMode().required(); + systemState = ess.systemState().required(); + setWorkState = ess.setWorkState().required(); + } + + public long useableSoc() throws InvalidValueException { + return soc.value() - minSoc.value(); + } + + public void start() { + currentState = WorkState.START; + // if (systemState.labelOptional().isPresent() && + // (systemState.labelOptional().equals(Optional.of(EssNature.STOP)) + // || systemState.labelOptional().equals(Optional.of(EssNature.STANDBY)))) { + // setWorkState.pushWriteFromLabel(EssNature.START); + // } + } + + public void standby() { + currentState = WorkState.STANDBY; + // if (systemState.labelOptional().isPresent() + // && systemState.labelOptional().equals(Optional.of(EssNature.START))) { + // setWorkState.pushWriteFromLabel(EssNature.STANDBY); + // } + } + + public void setWorkState() throws WriteChannelException { + if (currentState != null) { + switch (currentState) { + case STANDBY: + setWorkState.pushWriteFromLabel(EssNature.STANDBY); + break; + case START: + setWorkState.pushWriteFromLabel(EssNature.START); + break; + case STOP: + setWorkState.pushWriteFromLabel(EssNature.STOP); + break; + default: + setWorkState.pushWriteFromLabel(EssNature.STANDBY); + break; + } + } + } + +} diff --git a/edge/src/io/openems/impl/controller/supplybusswitch/SupplyBusSwitchController.java b/edge/src/io/openems/impl/controller/supplybusswitch/SupplyBusSwitchController.java index cd02c6e6356..df772244752 100644 --- a/edge/src/io/openems/impl/controller/supplybusswitch/SupplyBusSwitchController.java +++ b/edge/src/io/openems/impl/controller/supplybusswitch/SupplyBusSwitchController.java @@ -86,14 +86,15 @@ public void run() { for (Supplybus sb : supplybuses) { sb.run(); } - if (isOnGrid()) { - // start all ess - for (Ess ess : esss.value()) { - try { - ess.start(); - } catch (WriteChannelException e) { - log.error("Failed to start " + ess.id(), e); - } + for (Ess ess : esss.value()) { + if (ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + // start all ess + ess.start(); + } + try { + ess.setWorkState(); + } catch (WriteChannelException e) { + log.error("Can't set Workstate for ess[" + ess.id() + "]", e); } } } catch (InvalidValueException e1) { @@ -101,15 +102,6 @@ public void run() { } } - private boolean isOnGrid() throws InvalidValueException { - for (Ess ess : esss.value()) { - if (ess.gridMode.labelOptional().equals(Optional.of(EssNature.OFF_GRID))) { - return false; - } - } - return true; - } - @Override public void channelChanged(Channel channel, Optional newValue, Optional oldValue) { if (channel.equals(supplyBusConfig) || channel.equals(esss)) { diff --git a/edge/src/io/openems/impl/controller/supplybusswitch/Supplybus.java b/edge/src/io/openems/impl/controller/supplybusswitch/Supplybus.java index ddf04bfd044..323e9ace63d 100644 --- a/edge/src/io/openems/impl/controller/supplybusswitch/Supplybus.java +++ b/edge/src/io/openems/impl/controller/supplybusswitch/Supplybus.java @@ -124,7 +124,14 @@ public void run() throws InvalidValueException { case CONNECTING: { // if not connected send connect command again if (isConnected()) { - // TODO connect all loads after ess connected and started + if (supplybusOnIndication != null) { + try { + supplybusOnIndication.pushWrite(1L); + } catch (WriteChannelException e) { + log.error("can't set supplybusOnIndication", e); + } + } + // connect all loads after ess connected and started try { if (connectLoads()) { state = State.CONNECTED; @@ -148,31 +155,25 @@ public void run() throws InvalidValueException { // only connect if soc is larger than minSoc + 5 or Ess is On-Grid if (mostLoad != null) { if (mostLoad.soc.value() > mostLoad.minSoc.value() + 5) { - try { - // connect(mostLoad); - activeEss = mostLoad; - activeEss.start(); - activeEss.setActiveSupplybus(this); - lastTimeDisconnected = System.currentTimeMillis(); - state = State.CONNECTING; - } catch (WriteChannelException e) { - log.error("Can't start ess[" + activeEss.id() + "]", e); - } + // connect(mostLoad); + activeEss = mostLoad; + activeEss.start(); + activeEss.setActiveSupplybus(this); + lastTimeDisconnected = System.currentTimeMillis(); + state = State.CONNECTING; } } else { // all ess empty check if On-Grid List onGridEss = getOnGridEss(); if (onGridEss.size() > 0) { - try { - // connect(mostLoad); - activeEss = onGridEss.get(0); - activeEss.start(); - activeEss.setActiveSupplybus(this); - lastTimeDisconnected = System.currentTimeMillis(); - state = State.CONNECTING; - } catch (WriteChannelException e) { - log.error("Can't start ess[" + activeEss.id() + "]", e); - } + // connect(mostLoad); + activeEss = onGridEss.get(0); + activeEss.start(); + activeEss.setActiveSupplybus(this); + lastTimeDisconnected = System.currentTimeMillis(); + state = State.CONNECTING; + } else { + log.error("no ess to connect"); } } if (supplybusOnIndication != null) { @@ -189,18 +190,14 @@ public void run() throws InvalidValueException { if (isDisconnected()) { state = State.DISCONNECTED; } else { - // TODO disconnect all loads before disconnection + // disconnect all loads before disconnection try { if (disconnectLoads()) { disconnect(); try { Ess active = getActiveEss(); - try { - if (active != null && !active.equals(primaryEss)) { - active.standby(); - } - } catch (WriteChannelException e) { - log.error("Can't stop ess[" + active.id() + "]", e); + if (active != null && !active.equals(primaryEss)) { + active.standby(); } } catch (SupplyBusException e) { log.error("get Active Ess failed!", e); @@ -322,9 +319,9 @@ public Ess getLargestSoc() { iter.remove(); } } - if (esss.size() > 1) { - esss.remove(primaryEss); - } + // if (esss.size() > 1) { + // esss.remove(primaryEss); + // } Ess largestSoc = null; for (Ess ess : esss) { try { diff --git a/edge/src/io/openems/impl/controller/symmetric/balancing/BalancingController.java b/edge/src/io/openems/impl/controller/symmetric/balancing/BalancingController.java index 6960d4ec05d..c67333385cd 100644 --- a/edge/src/io/openems/impl/controller/symmetric/balancing/BalancingController.java +++ b/edge/src/io/openems/impl/controller/symmetric/balancing/BalancingController.java @@ -1,188 +1,196 @@ -/******************************************************************************* - * OpenEMS - Open Source Energy Management System - * Copyright (c) 2016, 2017 FENECON GmbH and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Contributors: - * FENECON GmbH - initial API and implementation and initial documentation - *******************************************************************************/ -package io.openems.impl.controller.symmetric.balancing; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import io.openems.api.channel.ConfigChannel; -import io.openems.api.controller.Controller; -import io.openems.api.device.nature.ess.SymmetricEssNature; -import io.openems.api.doc.ChannelInfo; -import io.openems.api.doc.ThingInfo; -import io.openems.api.exception.InvalidValueException; - -@ThingInfo(title = "Self-consumption optimization (Symmetric)", description = "Tries to keep the grid meter on zero. For symmetric Ess. Ess-Cluster is supported.") -public class BalancingController extends Controller { - - /* - * Constructors - */ - public BalancingController() { - super(); - } - - public BalancingController(String thingId) { - super(thingId); - } - - /* - * Config - */ - @ChannelInfo(title = "Ess", description = "Sets the Ess devices.", type = Ess.class, isArray = true) - public final ConfigChannel> esss = new ConfigChannel>("esss", this); - - @ChannelInfo(title = "Grid-Meter", description = "Sets the grid meter.", type = Meter.class) - public final ConfigChannel meter = new ConfigChannel("meter", this); - - /* - * Methods - */ - private boolean isOnGrid() throws InvalidValueException { - for (Ess ess : esss.value()) { - Optional gridMode = ess.gridMode.labelOptional(); - if (gridMode.isPresent() && !gridMode.get().equals(SymmetricEssNature.ON_GRID)) { - return false; - } - } - return true; - } - - @Override - public void run() { - try { - // Run only if all ess are on-grid - if (isOnGrid()) { - // Calculate required sum values - long calculatedPower = meter.value().activePower.value(); - long maxChargePower = 0; - long maxDischargePower = 0; - long useableSoc = 0; - for (Ess ess : esss.value()) { - calculatedPower += ess.activePower.value(); - maxChargePower += ess.setActivePower.writeMin().orElse(ess.allowedCharge.value()); - maxDischargePower += ess.setActivePower.writeMax().orElse(ess.allowedDischarge.value()); - useableSoc += ess.useableSoc(); - } - if (calculatedPower > 0) { - /* - * Discharge - */ - if (calculatedPower > maxDischargePower) { - calculatedPower = maxDischargePower; - } - // sort ess by useableSoc asc - Collections.sort(esss.value(), (a, b) -> { - try { - return (int) (a.useableSoc() - b.useableSoc()); - } catch (InvalidValueException e) { - log.error(e.getMessage()); - return 0; - } - }); - for (int i = 0; i < esss.value().size(); i++) { - Ess ess = esss.value().get(i); - // calculate minimal power needed to fulfill the calculatedPower - long minP = calculatedPower; - for (int j = i + 1; j < esss.value().size(); j++) { - if (esss.value().get(j).useableSoc() > 0) { - minP -= esss.value().get(j).allowedDischarge.value(); - } - } - if (minP < 0) { - minP = 0; - } - // check maximal power to avoid larger charges then calculatedPower - long maxP = ess.allowedDischarge.value(); - if (calculatedPower < maxP) { - maxP = calculatedPower; - } - double diff = maxP - minP; - /* - * weight the range of possible power by the useableSoc - * if the useableSoc is negative the ess will be charged - */ - long p = (long) (Math.ceil((minP + diff / useableSoc * ess.useableSoc()) / 100) * 100); - ess.power.setActivePower(p); - ess.power.writePower(); - log.debug(ess.id() + " Set ActivePower [" + ess.power.getActivePower() + "], ReactivePower [" - + ess.power.getReactivePower() + "]"); - calculatedPower -= p; - } - } else { - /* - * Charge - */ - if (calculatedPower < maxChargePower) { - calculatedPower = maxChargePower; - } - /* - * sort ess by 100 - useabelSoc - * 100 - 90 = 10 - * 100 - 45 = 55 - * 100 - (- 5) = 105 - * => ess with negative useableSoc will be charged much more then one with positive useableSoc - */ - Collections.sort(esss.value(), (a, b) -> { - try { - return (int) ((100 - a.useableSoc()) - (100 - b.useableSoc())); - } catch (InvalidValueException e) { - log.error(e.getMessage()); - return 0; - } - }); - for (int i = 0; i < esss.value().size(); i++) { - Ess ess = esss.value().get(i); - // calculate minimal power needed to fulfill the calculatedPower - long minP = calculatedPower; - for (int j = i + 1; j < esss.value().size(); j++) { - minP -= esss.value().get(j).allowedCharge.value(); - } - if (minP > 0) { - minP = 0; - } - // check maximal power to avoid larger charges then calculatedPower - long maxP = ess.allowedCharge.value(); - if (calculatedPower > maxP) { - maxP = calculatedPower; - } - double diff = maxP - minP; - // weight the range of possible power by the useableSoc - long p = (long) Math.floor( - (minP + diff / (esss.value().size() * 100 - useableSoc) * (100 - ess.useableSoc())) - / 100) - * 100; - ess.power.setActivePower(p); - ess.power.writePower(); - log.debug(ess.id() + " Set ActivePower [" + ess.power.getActivePower() + "], ReactivePower [" - + ess.power.getReactivePower() + "]"); - calculatedPower -= p; - } - } - - } - } catch (InvalidValueException e) { - log.error(e.getMessage()); - } - } - -} +/******************************************************************************* + * OpenEMS - Open Source Energy Management System + * Copyright (c) 2016, 2017 FENECON GmbH and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Contributors: + * FENECON GmbH - initial API and implementation and initial documentation + *******************************************************************************/ +package io.openems.impl.controller.symmetric.balancing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.controller.Controller; +import io.openems.api.device.nature.ess.EssNature; +import io.openems.api.doc.ChannelInfo; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.InvalidValueException; +import io.openems.core.utilities.AvgFiFoQueue; + +@ThingInfo(title = "Self-consumption optimization (Symmetric)", description = "Tries to keep the grid meter on zero. For symmetric Ess. Ess-Cluster is supported.") +public class BalancingController extends Controller { + + /* + * Constructors + */ + public BalancingController() { + super(); + } + + public BalancingController(String thingId) { + super(thingId); + } + + /* + * Config + */ + @ChannelInfo(title = "Ess", description = "Sets the Ess devices.", type = Ess.class, isArray = true) + public final ConfigChannel> esss = new ConfigChannel>("esss", this); + + @ChannelInfo(title = "Grid-Meter", description = "Sets the grid meter.", type = Meter.class) + public final ConfigChannel meter = new ConfigChannel("meter", this); + + private AvgFiFoQueue meterPower = new AvgFiFoQueue(2, 1.5); + + /* + * Methods + */ + + @Override + public void run() { + try { + // Run only if at least one ess is on-grid + List useableEss = getUseableEss(); + if (useableEss.size() > 0) { + // Calculate required sum values + meterPower.add(meter.value().activePower.value()); + long calculatedPower = meterPower.avg(); + long maxChargePower = 0; + long maxDischargePower = 0; + long useableSoc = 0; + for (Ess ess : useableEss) { + ess.powerAvg.add(ess.activePower.value()); + calculatedPower += ess.powerAvg.avg(); + maxChargePower += ess.setActivePower.writeMin().orElse(ess.allowedCharge.value()); + maxDischargePower += ess.setActivePower.writeMax().orElse(ess.allowedDischarge.value()); + useableSoc += ess.useableSoc(); + } + if (calculatedPower > 0) { + /* + * Discharge + */ + if (calculatedPower > maxDischargePower) { + calculatedPower = maxDischargePower; + } + // sort ess by useableSoc asc + Collections.sort(useableEss, (a, b) -> { + try { + return (int) (a.useableSoc() - b.useableSoc()); + } catch (InvalidValueException e) { + log.error(e.getMessage()); + return 0; + } + }); + for (int i = 0; i < useableEss.size(); i++) { + Ess ess = useableEss.get(i); + // calculate minimal power needed to fulfill the calculatedPower + long minP = calculatedPower; + for (int j = i + 1; j < useableEss.size(); j++) { + if (useableEss.get(j).useableSoc() > 0) { + minP -= useableEss.get(j).allowedDischarge.value(); + } + } + if (minP < 0) { + minP = 0; + } + // check maximal power to avoid larger charges then calculatedPower + long maxP = ess.allowedDischarge.value(); + if (calculatedPower < maxP) { + maxP = calculatedPower; + } + double diff = maxP - minP; + /* + * weight the range of possible power by the useableSoc + * if the useableSoc is negative the ess will be charged + */ + long p = (long) (Math.ceil((minP + diff / useableSoc * ess.useableSoc()) / 100) * 100); + ess.power.setActivePower(p); + ess.power.writePower(); + log.debug(ess.id() + " Set ActivePower [" + ess.power.getActivePower() + "], ReactivePower [" + + ess.power.getReactivePower() + "]"); + calculatedPower -= p; + } + } else { + /* + * Charge + */ + if (calculatedPower < maxChargePower) { + calculatedPower = maxChargePower; + } + /* + * sort ess by 100 - useabelSoc + * 100 - 90 = 10 + * 100 - 45 = 55 + * 100 - (- 5) = 105 + * => ess with negative useableSoc will be charged much more then one with positive useableSoc + */ + Collections.sort(useableEss, (a, b) -> { + try { + return (int) ((100 - a.useableSoc()) - (100 - b.useableSoc())); + } catch (InvalidValueException e) { + log.error(e.getMessage()); + return 0; + } + }); + for (int i = 0; i < useableEss.size(); i++) { + Ess ess = useableEss.get(i); + // calculate minimal power needed to fulfill the calculatedPower + long minP = calculatedPower; + for (int j = i + 1; j < useableEss.size(); j++) { + minP -= useableEss.get(j).allowedCharge.value(); + } + if (minP > 0) { + minP = 0; + } + // check maximal power to avoid larger charges then calculatedPower + long maxP = ess.allowedCharge.value(); + if (calculatedPower > maxP) { + maxP = calculatedPower; + } + double diff = maxP - minP; + // weight the range of possible power by the useableSoc + long p = (long) Math.floor( + (minP + diff / (useableEss.size() * 100 - useableSoc) * (100 - ess.useableSoc())) / 100) + * 100; + ess.power.setActivePower(p); + ess.power.writePower(); + log.debug(ess.id() + " Set ActivePower [" + ess.power.getActivePower() + "], ReactivePower [" + + ess.power.getReactivePower() + "]"); + calculatedPower -= p; + } + } + + } + } catch (InvalidValueException e) { + log.error(e.getMessage()); + } + } + + private List getUseableEss() throws InvalidValueException { + List useableEss = new ArrayList<>(); + for (Ess ess : esss.value()) { + if (ess.gridMode.valueOptional().isPresent() + && ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + useableEss.add(ess); + } + } + return useableEss; + } + +} diff --git a/edge/src/io/openems/impl/controller/symmetric/balancing/Ess.java b/edge/src/io/openems/impl/controller/symmetric/balancing/Ess.java index aaccf78c87c..4c20c02d3c0 100644 --- a/edge/src/io/openems/impl/controller/symmetric/balancing/Ess.java +++ b/edge/src/io/openems/impl/controller/symmetric/balancing/Ess.java @@ -1,65 +1,67 @@ -/******************************************************************************* - * OpenEMS - Open Source Energy Management System - * Copyright (c) 2016 FENECON GmbH and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Contributors: - * FENECON GmbH - initial API and implementation and initial documentation - *******************************************************************************/ -package io.openems.impl.controller.symmetric.balancing; - -import io.openems.api.channel.ReadChannel; -import io.openems.api.channel.WriteChannel; -import io.openems.api.controller.IsThingMap; -import io.openems.api.controller.ThingMap; -import io.openems.api.device.nature.ess.SymmetricEssNature; -import io.openems.api.exception.InvalidValueException; -import io.openems.core.utilities.SymmetricPower; - -@IsThingMap(type = SymmetricEssNature.class) -public class Ess extends ThingMap { - - public final ReadChannel minSoc; - public final WriteChannel setActivePower; - public final WriteChannel setReactivePower; - public final ReadChannel soc; - public final ReadChannel activePower; - public final ReadChannel allowedCharge; - public final ReadChannel allowedDischarge; - public final ReadChannel gridMode; - public final ReadChannel systemState; - public final SymmetricPower power; - - public Ess(SymmetricEssNature ess) { - super(ess); - minSoc = ess.minSoc().required(); - - setActivePower = ess.setActivePower().required(); - setReactivePower = ess.setReactivePower().required(); - - soc = ess.soc().required(); - activePower = ess.activePower().required(); - allowedCharge = ess.allowedCharge().required(); - allowedDischarge = ess.allowedDischarge().required(); - gridMode = ess.gridMode().required(); - systemState = ess.systemState().required(); - this.power = new SymmetricPower(ess.allowedDischarge().required(), ess.allowedCharge().required(), - ess.allowedApparent().required(), ess.setActivePower().required(), ess.setReactivePower().required()); - } - - public long useableSoc() throws InvalidValueException { - return soc.value() - minSoc.value(); - } -} +/******************************************************************************* + * OpenEMS - Open Source Energy Management System + * Copyright (c) 2016 FENECON GmbH and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Contributors: + * FENECON GmbH - initial API and implementation and initial documentation + *******************************************************************************/ +package io.openems.impl.controller.symmetric.balancing; + +import io.openems.api.channel.ReadChannel; +import io.openems.api.channel.WriteChannel; +import io.openems.api.controller.IsThingMap; +import io.openems.api.controller.ThingMap; +import io.openems.api.device.nature.ess.SymmetricEssNature; +import io.openems.api.exception.InvalidValueException; +import io.openems.core.utilities.AvgFiFoQueue; +import io.openems.core.utilities.SymmetricPower; + +@IsThingMap(type = SymmetricEssNature.class) +public class Ess extends ThingMap { + + public final ReadChannel minSoc; + public final WriteChannel setActivePower; + public final WriteChannel setReactivePower; + public final ReadChannel soc; + public final ReadChannel activePower; + public final ReadChannel allowedCharge; + public final ReadChannel allowedDischarge; + public final ReadChannel gridMode; + public final ReadChannel systemState; + public final SymmetricPower power; + public AvgFiFoQueue powerAvg = new AvgFiFoQueue(2, 1.5); + + public Ess(SymmetricEssNature ess) { + super(ess); + minSoc = ess.minSoc().required(); + + setActivePower = ess.setActivePower().required(); + setReactivePower = ess.setReactivePower().required(); + + soc = ess.soc().required(); + activePower = ess.activePower().required(); + allowedCharge = ess.allowedCharge().required(); + allowedDischarge = ess.allowedDischarge().required(); + gridMode = ess.gridMode().required(); + systemState = ess.systemState().required(); + this.power = new SymmetricPower(ess.allowedDischarge().required(), ess.allowedCharge().required(), + ess.allowedApparent().required(), ess.setActivePower().required(), ess.setReactivePower().required()); + } + + public long useableSoc() throws InvalidValueException { + return soc.value() - minSoc.value(); + } +} diff --git a/edge/src/io/openems/impl/controller/symmetric/timelinecharge/TimelineChargeController.java b/edge/src/io/openems/impl/controller/symmetric/timelinecharge/TimelineChargeController.java index b7cc9ebff9f..699fbb8c553 100644 --- a/edge/src/io/openems/impl/controller/symmetric/timelinecharge/TimelineChargeController.java +++ b/edge/src/io/openems/impl/controller/symmetric/timelinecharge/TimelineChargeController.java @@ -1,390 +1,394 @@ -/******************************************************************************* - * OpenEMS - Open Source Energy Management System - * Copyright (c) 2016, 2017 FENECON GmbH and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Contributors: - * FENECON GmbH - initial API and implementation and initial documentation - *******************************************************************************/ -package io.openems.impl.controller.symmetric.timelinecharge; - -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import io.openems.api.channel.ConfigChannel; -import io.openems.api.controller.Controller; -import io.openems.api.doc.ChannelInfo; -import io.openems.api.doc.ThingInfo; -import io.openems.api.exception.ConfigException; -import io.openems.api.exception.InvalidValueException; -import io.openems.api.exception.ReflectionException; -import io.openems.api.exception.WriteChannelException; -import io.openems.api.security.User; -import io.openems.core.utilities.AvgFiFoQueue; -import io.openems.core.utilities.ControllerUtils; -import io.openems.core.utilities.JsonUtils; - -@ThingInfo(title = "Timeline charge (Symmetric)") -public class TimelineChargeController extends Controller { - - /* - * Constructors - */ - public TimelineChargeController() { - super(); - } - - public TimelineChargeController(String thingId) { - super(thingId); - } - - /* - * Config - */ - @ChannelInfo(title = "Ess", description = "Sets the Ess device.", type = Ess.class) - public final ConfigChannel ess = new ConfigChannel("ess", this); - - @ChannelInfo(title = "Grid-Meter", description = "Sets the grid meter.", type = Meter.class) - public final ConfigChannel meter = new ConfigChannel<>("meter", this); - - @ChannelInfo(title = "Max-ApparentPower", description = "How much apparent power the grid connection can take.", type = Long.class) - public final ConfigChannel allowedApparent = new ConfigChannel<>("allowedApparent", this); - - @ChannelInfo(title = "Charger", description = "Sets the Chargers connected to the ess.", type = Charger.class, isArray = true) - public final ConfigChannel> chargers = new ConfigChannel>("chargers", this); - - @ChannelInfo(title = "Monday", description = "Sets the soc limits for monday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel monday = new ConfigChannel<>("monday", this); - - @ChannelInfo(title = "Tuesday", description = "Sets the soc limits for tuesday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel tuesday = new ConfigChannel<>("tuesday", this); - - @ChannelInfo(title = "Wednesday", description = "Sets the soc limits for wednesday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel wednesday = new ConfigChannel<>("wednesday", this); - - @ChannelInfo(title = "Thursday", description = "Sets the soc limits for thursday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel thursday = new ConfigChannel<>("thursday", this); - - @ChannelInfo(title = "Friday", description = "Sets the soc limits for friday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel friday = new ConfigChannel<>("friday", this); - - @ChannelInfo(title = "Saturday", description = "Sets the soc limits for saturday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel saturday = new ConfigChannel<>("saturday", this); - - @ChannelInfo(title = "Sunday", description = "Sets the soc limits for sunday.", type = JsonArray.class, accessLevel = User.OWNER) - public ConfigChannel sunday = new ConfigChannel<>("sunday", this); - - /* - * Fields - */ - private AvgFiFoQueue floatingChargerPower = new AvgFiFoQueue(10, 1); - private State currentState = State.NORMAL; - - public enum State { - NORMAL, MINSOC, CHARGESOC - } - - /* - * Methods - */ - @Override - public void run() { - try { - Ess ess = this.ess.value(); - long allowedApparentCharge = allowedApparent.value() - meter.value().apparentPower.value(); - allowedApparentCharge += ControllerUtils.calculateApparentPower(ess.activePower.valueOptional().orElse(0L), - ess.reactivePower.valueOptional().orElse(0L)); - // remove 10% for tollerance - allowedApparentCharge *= 0.9; - // limit activePower to apparent - try { - ess.setActivePower.pushWriteMin(allowedApparentCharge * -1); - } catch (WriteChannelException e) { - log.warn("Failed to set writeMin to " + (allowedApparentCharge * -1)); - } - long chargerPower = 0L; - for (Charger c : chargers.value()) { - try { - chargerPower += c.power.value(); - } catch (InvalidValueException e) { - log.error("cant read power from " + c.id(), e); - } - } - floatingChargerPower.add(chargerPower); - SocPoint socPoint = getSoc(); - double requiredEnergy = ((double) ess.capacity.value() / 100.0 * socPoint.getSoc()) - - ((double) ess.capacity.value() / 100.0 * ess.soc.value()); - long requiredTimeCharger = (long) (requiredEnergy / floatingChargerPower.avg() * 3600.0); - long requiredTimeGrid = (long) (requiredEnergy / (floatingChargerPower.avg() + allowedApparentCharge) - * 3600.0); - log.info("RequiredTimeCharger: " + requiredTimeCharger + ", RequiredTimeGrid: " + requiredTimeGrid); - if (floatingChargerPower.avg() >= 1000 - && !LocalDateTime.now().plusSeconds(requiredTimeCharger).isBefore(socPoint.getTime()) - && LocalDateTime.now().plusSeconds(requiredTimeGrid).isBefore(socPoint.getTime())) { - // Prevent discharge -> load with Pv - ess.setActivePower.pushWriteMax(0L); - } else if (!LocalDateTime.now().plusSeconds(requiredTimeGrid).isBefore(socPoint.getTime()) - && socPoint.getTime().isAfter(LocalDateTime.now())) { - // Load with grid + pv - long maxPower = allowedApparentCharge * -1; - if (ess.setActivePower.writeMin().isPresent() && ess.setActivePower.writeMin().get() > maxPower) { - maxPower = ess.setActivePower.writeMin().get(); - } - ess.setActivePower.pushWriteMax(maxPower); - } else { - // soc point in the past -> Hold load - int minSoc = getCurrentSoc().getSoc(); - int chargeSoc = minSoc - 5; - if (chargeSoc <= 1) { - chargeSoc = 1; - } - switch (currentState) { - case CHARGESOC: - if (ess.soc.value() > minSoc) { - currentState = State.MINSOC; - } else { - try { - Optional currentMinValue = ess.setActivePower.writeMin(); - if (currentMinValue.isPresent() && currentMinValue.get() < 0) { - // Force Charge with minimum of MaxChargePower/5 - log.info("Force charge. Set ActivePower=Max[" + currentMinValue.get() / 5 + "]"); - ess.setActivePower.pushWriteMax(currentMinValue.get() / 5); - } else { - log.info("Avoid discharge. Set ActivePower=Max[-1000 W]"); - ess.setActivePower.pushWriteMax(-1000L); - } - } catch (WriteChannelException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - break; - case MINSOC: - if (ess.soc.value() < chargeSoc) { - currentState = State.CHARGESOC; - } else if (ess.soc.value() >= minSoc + 5) { - currentState = State.NORMAL; - } else { - try { - long maxPower = 0; - if (!ess.setActivePower.writeMax().isPresent() - || maxPower < ess.setActivePower.writeMax().get()) { - ess.setActivePower.pushWriteMax(maxPower); - } - } catch (WriteChannelException e) { - log.error(ess.id() + "Failed to set Max allowed power.", e); - } - } - break; - case NORMAL: - if (ess.soc.value() <= minSoc) { - currentState = State.MINSOC; - } - break; - } - } - } catch (InvalidValueException e) { - log.error("Can't read value", e); - } catch (WriteChannelException e) { - log.error("Can't write value", e); - } - } - - // private Entry getSoc() { - // Entry lastSocPoint = socPoints.floorEntry(LocalDateTime.now()); - // Entry nextSocPoint = socPoints.higherEntry(LocalDateTime.now()); - // if (nextSocPoint != null) { - // return nextSocPoint; - // } - // return lastSocPoint; - // } - - // private int getCurrentSoc() { - // Entry socPoint = socPoints.floorEntry(LocalDateTime.now()); - // return socPoint.getValue(); - // } - - private JsonArray getJsonOfDay(DayOfWeek day) throws InvalidValueException { - switch (day) { - case FRIDAY: - return friday.value(); - case SATURDAY: - return saturday.value(); - case SUNDAY: - return sunday.value(); - case THURSDAY: - return thursday.value(); - case TUESDAY: - return tuesday.value(); - case WEDNESDAY: - return wednesday.value(); - default: - case MONDAY: - return monday.value(); - } - } - - // private Integer getCurrentSoc() throws InvalidValueException { - // Integer soc = null; - // try { - // JsonArray jHours = getJsonOfDay(LocalDate.now().getDayOfWeek()); - // LocalTime time = LocalTime.now(); - // int count = 1; - // while (soc == null && count < 8) { - // try { - // Entry entry = floorSoc(jHours, time); - // if (entry != null) { - // soc = entry.getValue(); - // } - // } catch (IndexOutOfBoundsException e) { - // time = LocalTime.MAX; - // jHours = getJsonOfDay(LocalDate.now().getDayOfWeek().minus(count)); - // } - // count++; - // } - // } catch (ConfigException e) { - // log.error("failed to find soc", e); - // } - // return soc; - // } - - private SocPoint getCurrentSoc() { - SocPoint soc = null; - JsonArray jHours; - try { - jHours = getJsonOfDay(LocalDate.now().getDayOfWeek()); - LocalTime time = LocalTime.now(); - int count = 1; - while (soc == null && count < 8) { - try { - Entry entry = floorSoc(jHours, time); - soc = new SocPoint(LocalDateTime.of(LocalDate.now().minusDays(count), entry.getKey()), - entry.getValue()); - } catch (IndexOutOfBoundsException e) { - time = LocalTime.MIN; - jHours = getJsonOfDay(LocalDate.now().getDayOfWeek().minus(count)); - } - count++; - } - } catch (InvalidValueException | ConfigException e1) { - log.error("failed to find soc", e1); - } - if (soc == null) { - soc = new SocPoint(LocalDateTime.MIN, 10); - } - return soc; - } - - private SocPoint getSoc() { - SocPoint soc = null; - JsonArray jHours; - try { - jHours = getJsonOfDay(LocalDate.now().getDayOfWeek()); - LocalTime time = LocalTime.now(); - int count = 1; - while (soc == null && count < 8) { - try { - Entry entry = higherSoc(jHours, time); - soc = new SocPoint(LocalDateTime.of(LocalDate.now().plusDays(count - 1), entry.getKey()), - entry.getValue()); - } catch (IndexOutOfBoundsException e) { - time = LocalTime.MIN; - jHours = getJsonOfDay(LocalDate.now().getDayOfWeek().plus(count)); - } - count++; - } - } catch (InvalidValueException | ConfigException e1) { - log.error("failed to find soc", e1); - } - if (soc == null) { - soc = new SocPoint(LocalDateTime.MIN, 10); - } - return soc; - } - - private Entry floorSoc(JsonArray jHours, LocalTime time) throws ConfigException { - try { - // fill times map; sorted by hour - TreeMap times = new TreeMap<>(); - for (JsonElement jHourElement : jHours) { - JsonObject jHour = JsonUtils.getAsJsonObject(jHourElement); - String hourTime = JsonUtils.getAsString(jHour, "time"); - int jsoc = JsonUtils.getAsInt(jHourElement, "soc"); - times.put(LocalTime.parse(hourTime), jsoc); - } - // return matching controllers - if (times.floorEntry(time) != null) { - return times.floorEntry(time); - } else { - throw new IndexOutOfBoundsException("No smaller time found"); - } - } catch (ReflectionException e) { - throw new ConfigException("cant read config", e); - } - } - - private Map.Entry higherSoc(JsonArray jHours, LocalTime time) throws ConfigException { - // fill times map; sorted by hour - try { - TreeMap times = new TreeMap<>(); - for (JsonElement jHourElement : jHours) { - JsonObject jHour = JsonUtils.getAsJsonObject(jHourElement); - String hourTime = JsonUtils.getAsString(jHour, "time"); - int jsoc = JsonUtils.getAsInt(jHourElement, "soc"); - times.put(LocalTime.parse(hourTime), jsoc); - } - // return matching controllers - if (times.higherEntry(time) != null) { - return times.higherEntry(time); - } else { - throw new IndexOutOfBoundsException("No smaller time found"); - } - } catch (ReflectionException e) { - throw new ConfigException("cant read config", e); - } - } - - private class SocPoint { - private final LocalDateTime time; - private final int soc; - - public SocPoint(java.time.LocalDateTime time, int soc) { - super(); - this.time = time; - this.soc = soc; - } - - public LocalDateTime getTime() { - return time; - } - - public int getSoc() { - return soc; - } - - } - -} +/******************************************************************************* + * OpenEMS - Open Source Energy Management System + * Copyright (c) 2016, 2017 FENECON GmbH and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Contributors: + * FENECON GmbH - initial API and implementation and initial documentation + *******************************************************************************/ +package io.openems.impl.controller.symmetric.timelinecharge; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.controller.Controller; +import io.openems.api.device.nature.ess.EssNature; +import io.openems.api.doc.ChannelInfo; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.ConfigException; +import io.openems.api.exception.InvalidValueException; +import io.openems.api.exception.ReflectionException; +import io.openems.api.exception.WriteChannelException; +import io.openems.api.security.User; +import io.openems.core.utilities.AvgFiFoQueue; +import io.openems.core.utilities.ControllerUtils; +import io.openems.core.utilities.JsonUtils; + +@ThingInfo(title = "Timeline charge (Symmetric)") +public class TimelineChargeController extends Controller { + + /* + * Constructors + */ + public TimelineChargeController() { + super(); + } + + public TimelineChargeController(String thingId) { + super(thingId); + } + + /* + * Config + */ + @ChannelInfo(title = "Ess", description = "Sets the Ess device.", type = Ess.class) + public final ConfigChannel ess = new ConfigChannel("ess", this); + + @ChannelInfo(title = "Grid-Meter", description = "Sets the grid meter.", type = Meter.class) + public final ConfigChannel meter = new ConfigChannel<>("meter", this); + + @ChannelInfo(title = "Max-ApparentPower", description = "How much apparent power the grid connection can take.", type = Long.class) + public final ConfigChannel allowedApparent = new ConfigChannel<>("allowedApparent", this); + + @ChannelInfo(title = "Charger", description = "Sets the Chargers connected to the ess.", type = Charger.class, isArray = true) + public final ConfigChannel> chargers = new ConfigChannel>("chargers", this); + + @ChannelInfo(title = "Monday", description = "Sets the soc limits for monday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel monday = new ConfigChannel<>("monday", this); + + @ChannelInfo(title = "Tuesday", description = "Sets the soc limits for tuesday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel tuesday = new ConfigChannel<>("tuesday", this); + + @ChannelInfo(title = "Wednesday", description = "Sets the soc limits for wednesday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel wednesday = new ConfigChannel<>("wednesday", this); + + @ChannelInfo(title = "Thursday", description = "Sets the soc limits for thursday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel thursday = new ConfigChannel<>("thursday", this); + + @ChannelInfo(title = "Friday", description = "Sets the soc limits for friday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel friday = new ConfigChannel<>("friday", this); + + @ChannelInfo(title = "Saturday", description = "Sets the soc limits for saturday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel saturday = new ConfigChannel<>("saturday", this); + + @ChannelInfo(title = "Sunday", description = "Sets the soc limits for sunday.", type = JsonArray.class, accessLevel = User.OWNER) + public ConfigChannel sunday = new ConfigChannel<>("sunday", this); + + /* + * Fields + */ + private AvgFiFoQueue floatingChargerPower = new AvgFiFoQueue(10, 1); + private State currentState = State.NORMAL; + + public enum State { + NORMAL, MINSOC, CHARGESOC + } + + /* + * Methods + */ + @Override + public void run() { + try { + Ess ess = this.ess.value(); + if (ess.gridMode.labelOptional().equals(Optional.of(EssNature.ON_GRID))) { + long allowedApparentCharge = allowedApparent.value() - meter.value().apparentPower.value(); + allowedApparentCharge += ControllerUtils.calculateApparentPower( + ess.activePower.valueOptional().orElse(0L), ess.reactivePower.valueOptional().orElse(0L)); + // remove 10% for tollerance + allowedApparentCharge *= 0.9; + // limit activePower to apparent + try { + ess.setActivePower.pushWriteMin(allowedApparentCharge * -1); + } catch (WriteChannelException e) { + log.warn("Failed to set writeMin to " + (allowedApparentCharge * -1)); + } + long chargerPower = 0L; + for (Charger c : chargers.value()) { + try { + chargerPower += c.power.value(); + } catch (InvalidValueException e) { + log.error("cant read power from " + c.id(), e); + } + } + floatingChargerPower.add(chargerPower); + SocPoint socPoint = getSoc(); + double requiredEnergy = ((double) ess.capacity.value() / 100.0 * socPoint.getSoc()) + - ((double) ess.capacity.value() / 100.0 * ess.soc.value()); + long requiredTimeCharger = (long) (requiredEnergy / floatingChargerPower.avg() * 3600.0); + long requiredTimeGrid = (long) (requiredEnergy / (floatingChargerPower.avg() + allowedApparentCharge) + * 3600.0); + log.info("RequiredTimeCharger: " + requiredTimeCharger + ", RequiredTimeGrid: " + requiredTimeGrid); + if (floatingChargerPower.avg() >= 1000 + && !LocalDateTime.now().plusSeconds(requiredTimeCharger).isBefore(socPoint.getTime()) + && LocalDateTime.now().plusSeconds(requiredTimeGrid).isBefore(socPoint.getTime())) { + // Prevent discharge -> load with Pv + ess.setActivePower.pushWriteMax(0L); + } else if (requiredTimeGrid > 0 + && !LocalDateTime.now().plusSeconds(requiredTimeGrid).isBefore(socPoint.getTime()) + && socPoint.getTime().isAfter(LocalDateTime.now())) { + // Load with grid + pv + long maxPower = allowedApparentCharge * -1; + if (ess.setActivePower.writeMin().isPresent() && ess.setActivePower.writeMin().get() > maxPower) { + maxPower = ess.setActivePower.writeMin().get(); + } + ess.setActivePower.pushWriteMax(maxPower); + } else { + // soc point in the past -> Hold load + int minSoc = getCurrentSoc().getSoc(); + int chargeSoc = minSoc - 5; + if (chargeSoc <= 1) { + chargeSoc = 1; + } + switch (currentState) { + case CHARGESOC: + if (ess.soc.value() > minSoc) { + currentState = State.MINSOC; + } else { + try { + Optional currentMinValue = ess.setActivePower.writeMin(); + if (currentMinValue.isPresent() && currentMinValue.get() < 0) { + // Force Charge with minimum of MaxChargePower/5 + log.info("Force charge. Set ActivePower=Max[" + currentMinValue.get() / 5 + "]"); + ess.setActivePower.pushWriteMax(currentMinValue.get() / 5); + } else { + log.info("Avoid discharge. Set ActivePower=Max[-1000 W]"); + ess.setActivePower.pushWriteMax(-1000L); + } + } catch (WriteChannelException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + break; + case MINSOC: + if (ess.soc.value() < chargeSoc) { + currentState = State.CHARGESOC; + } else if (ess.soc.value() >= minSoc + 5) { + currentState = State.NORMAL; + } else { + try { + long maxPower = 0; + if (!ess.setActivePower.writeMax().isPresent() + || maxPower < ess.setActivePower.writeMax().get()) { + ess.setActivePower.pushWriteMax(maxPower); + } + } catch (WriteChannelException e) { + log.error(ess.id() + "Failed to set Max allowed power.", e); + } + } + break; + case NORMAL: + if (ess.soc.value() <= minSoc) { + currentState = State.MINSOC; + } + break; + } + } + } + } catch (InvalidValueException e) { + log.error("Can't read value", e); + } catch (WriteChannelException e) { + log.error("Can't write value", e); + } + } + + // private Entry getSoc() { + // Entry lastSocPoint = socPoints.floorEntry(LocalDateTime.now()); + // Entry nextSocPoint = socPoints.higherEntry(LocalDateTime.now()); + // if (nextSocPoint != null) { + // return nextSocPoint; + // } + // return lastSocPoint; + // } + + // private int getCurrentSoc() { + // Entry socPoint = socPoints.floorEntry(LocalDateTime.now()); + // return socPoint.getValue(); + // } + + private JsonArray getJsonOfDay(DayOfWeek day) throws InvalidValueException { + switch (day) { + case FRIDAY: + return friday.value(); + case SATURDAY: + return saturday.value(); + case SUNDAY: + return sunday.value(); + case THURSDAY: + return thursday.value(); + case TUESDAY: + return tuesday.value(); + case WEDNESDAY: + return wednesday.value(); + default: + case MONDAY: + return monday.value(); + } + } + + // private Integer getCurrentSoc() throws InvalidValueException { + // Integer soc = null; + // try { + // JsonArray jHours = getJsonOfDay(LocalDate.now().getDayOfWeek()); + // LocalTime time = LocalTime.now(); + // int count = 1; + // while (soc == null && count < 8) { + // try { + // Entry entry = floorSoc(jHours, time); + // if (entry != null) { + // soc = entry.getValue(); + // } + // } catch (IndexOutOfBoundsException e) { + // time = LocalTime.MAX; + // jHours = getJsonOfDay(LocalDate.now().getDayOfWeek().minus(count)); + // } + // count++; + // } + // } catch (ConfigException e) { + // log.error("failed to find soc", e); + // } + // return soc; + // } + + private SocPoint getCurrentSoc() { + SocPoint soc = null; + JsonArray jHours; + try { + jHours = getJsonOfDay(LocalDate.now().getDayOfWeek()); + LocalTime time = LocalTime.now(); + int count = 1; + while (soc == null && count < 8) { + try { + Entry entry = floorSoc(jHours, time); + soc = new SocPoint(LocalDateTime.of(LocalDate.now().minusDays(count), entry.getKey()), + entry.getValue()); + } catch (IndexOutOfBoundsException e) { + time = LocalTime.MIN; + jHours = getJsonOfDay(LocalDate.now().getDayOfWeek().minus(count)); + } + count++; + } + } catch (InvalidValueException | ConfigException e1) { + log.error("failed to find soc", e1); + } + if (soc == null) { + soc = new SocPoint(LocalDateTime.MIN, 10); + } + return soc; + } + + private SocPoint getSoc() { + SocPoint soc = null; + JsonArray jHours; + try { + jHours = getJsonOfDay(LocalDate.now().getDayOfWeek()); + LocalTime time = LocalTime.now(); + int count = 1; + while (soc == null && count < 8) { + try { + Entry entry = higherSoc(jHours, time); + soc = new SocPoint(LocalDateTime.of(LocalDate.now().plusDays(count - 1), entry.getKey()), + entry.getValue()); + } catch (IndexOutOfBoundsException e) { + time = LocalTime.MIN; + jHours = getJsonOfDay(LocalDate.now().getDayOfWeek().plus(count)); + } + count++; + } + } catch (InvalidValueException | ConfigException e1) { + log.error("failed to find soc", e1); + } + if (soc == null) { + soc = new SocPoint(LocalDateTime.MIN, 10); + } + return soc; + } + + private Entry floorSoc(JsonArray jHours, LocalTime time) throws ConfigException { + try { + // fill times map; sorted by hour + TreeMap times = new TreeMap<>(); + for (JsonElement jHourElement : jHours) { + JsonObject jHour = JsonUtils.getAsJsonObject(jHourElement); + String hourTime = JsonUtils.getAsString(jHour, "time"); + int jsoc = JsonUtils.getAsInt(jHourElement, "soc"); + times.put(LocalTime.parse(hourTime), jsoc); + } + // return matching controllers + if (times.floorEntry(time) != null) { + return times.floorEntry(time); + } else { + throw new IndexOutOfBoundsException("No smaller time found"); + } + } catch (ReflectionException e) { + throw new ConfigException("cant read config", e); + } + } + + private Map.Entry higherSoc(JsonArray jHours, LocalTime time) throws ConfigException { + // fill times map; sorted by hour + try { + TreeMap times = new TreeMap<>(); + for (JsonElement jHourElement : jHours) { + JsonObject jHour = JsonUtils.getAsJsonObject(jHourElement); + String hourTime = JsonUtils.getAsString(jHour, "time"); + int jsoc = JsonUtils.getAsInt(jHourElement, "soc"); + times.put(LocalTime.parse(hourTime), jsoc); + } + // return matching controllers + if (times.higherEntry(time) != null) { + return times.higherEntry(time); + } else { + throw new IndexOutOfBoundsException("No smaller time found"); + } + } catch (ReflectionException e) { + throw new ConfigException("cant read config", e); + } + } + + private class SocPoint { + private final LocalDateTime time; + private final int soc; + + public SocPoint(java.time.LocalDateTime time, int soc) { + super(); + this.time = time; + this.soc = soc; + } + + public LocalDateTime getTime() { + return time; + } + + public int getSoc() { + return soc; + } + + } + +} diff --git a/edge/src/io/openems/impl/device/commercial/FeneconCommercialEss.java b/edge/src/io/openems/impl/device/commercial/FeneconCommercialEss.java index 703fb1e7a98..b5809b1d2fe 100644 --- a/edge/src/io/openems/impl/device/commercial/FeneconCommercialEss.java +++ b/edge/src/io/openems/impl/device/commercial/FeneconCommercialEss.java @@ -77,7 +77,7 @@ public ConfigChannel chargeSoc() { * Inherited Channels */ private ModbusReadLongChannel soc; - private ModbusReadLongChannel inverterActivePower; + private ModbusReadLongChannel activePower; private ModbusReadLongChannel allowedCharge; private ModbusReadLongChannel allowedDischarge; private ModbusReadLongChannel apparentPower; @@ -99,7 +99,7 @@ public ModbusReadLongChannel soc() { @Override public ModbusReadLongChannel activePower() { - return inverterActivePower; + return activePower; } @Override @@ -195,7 +195,7 @@ public ReadChannel maxNominalPower() { public ModbusReadLongChannel ipmTemperatureL3; public ModbusReadLongChannel transformerTemperatureL2; public ModbusReadLongChannel allowedApparent; - public ModbusReadLongChannel activePower; + public ModbusReadLongChannel gridActivePower; public StatusBitChannel suggestiveInformation1; public StatusBitChannel suggestiveInformation2; public StatusBitChannel suggestiveInformation3; @@ -443,7 +443,8 @@ protected ModbusProtocol defineModbusProtocol() throws ConfigException { acDischargeEnergy = new ModbusReadLongChannel("AcDischargeEnergy", this).unit("Wh") .multiplier(2)).wordOrder(WordOrder.LSWMSW), new DummyElement(0x020C, 0x020F), new SignedWordElement(0x0210, // - activePower = new ModbusReadLongChannel("ActivePower", this).unit("W").multiplier(2)), + gridActivePower = new ModbusReadLongChannel("GridActivePower", this).unit("W") + .multiplier(2)), new SignedWordElement(0x0211, // reactivePower = new ModbusReadLongChannel("ReactivePower", this).unit("var") .multiplier(2)), @@ -485,8 +486,7 @@ protected ModbusProtocol defineModbusProtocol() throws ConfigException { inverterCurrentL3 = new ModbusReadLongChannel("InverterCurrentL3", this).unit("mA") .multiplier(2)), // new SignedWordElement(0x0228, // - inverterActivePower = new ModbusReadLongChannel("InverterActivePower", this).unit("W") - .multiplier(2)), // + activePower = new ModbusReadLongChannel("ActivePower", this).unit("W").multiplier(2)), // new DummyElement(0x0229, 0x022F), new SignedWordElement(0x0230, // allowedCharge = new ModbusReadLongChannel("AllowedCharge", this).unit("W") .multiplier(2)), // diff --git a/edge/src/io/openems/impl/device/custom/riedmann/Riedmann.java b/edge/src/io/openems/impl/device/custom/riedmann/Riedmann.java index 05ca1246889..1aae8f47974 100644 --- a/edge/src/io/openems/impl/device/custom/riedmann/Riedmann.java +++ b/edge/src/io/openems/impl/device/custom/riedmann/Riedmann.java @@ -14,8 +14,8 @@ @ThingInfo(title = "Custom: Riedmann PLC") public class Riedmann extends ModbusDevice { - @ChannelInfo(title = "", type = RiedmannNature.class) - public final ConfigChannel device = new ConfigChannel("device", this); + @ChannelInfo(title = "", type = RiedmannNatureImpl.class) + public final ConfigChannel device = new ConfigChannel("device", this); public Riedmann(Bridge parent) throws OpenemsException { super(parent); diff --git a/edge/src/io/openems/impl/device/mini/FeneconMini.java b/edge/src/io/openems/impl/device/mini/FeneconMini.java index 1d5f40f3660..1a3e21a08bd 100644 --- a/edge/src/io/openems/impl/device/mini/FeneconMini.java +++ b/edge/src/io/openems/impl/device/mini/FeneconMini.java @@ -31,7 +31,7 @@ import io.openems.api.exception.OpenemsException; import io.openems.impl.protocol.modbus.ModbusDevice; -@ThingInfo(title = "FENECON Pro") +@ThingInfo(title = "FENECON Mini") public class FeneconMini extends ModbusDevice { /* @@ -52,7 +52,7 @@ public FeneconMini(Bridge parent) throws OpenemsException { */ @Override public String toString() { - return "FeneconPro [ess=" + ess + ", getThingId()=" + id() + "]"; + return "FeneconMini [ess=" + ess + ", getThingId()=" + id() + "]"; } @Override diff --git a/edge/src/io/openems/impl/device/minireadonly/FeneconMini.java b/edge/src/io/openems/impl/device/minireadonly/FeneconMini.java new file mode 100644 index 00000000000..3f4f7b3965a --- /dev/null +++ b/edge/src/io/openems/impl/device/minireadonly/FeneconMini.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * OpenEMS - Open Source Energy Management System + * Copyright (c) 2016, 2017 FENECON GmbH and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Contributors: + * FENECON GmbH - initial API and implementation and initial documentation + *******************************************************************************/ +package io.openems.impl.device.minireadonly; + +import java.util.HashSet; +import java.util.Set; + +import io.openems.api.bridge.Bridge; +import io.openems.api.channel.ConfigChannel; +import io.openems.api.device.nature.DeviceNature; +import io.openems.api.doc.ChannelInfo; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.OpenemsException; +import io.openems.impl.protocol.modbus.ModbusDevice; + +@ThingInfo(title = "FENECON Mini") +public class FeneconMini extends ModbusDevice { + + /* + * Constructors + */ + public FeneconMini(Bridge parent) throws OpenemsException { + super(parent); + } + + /* + * Config + */ + @ChannelInfo(title = "Ess", description = "Sets the Ess nature.", type = FeneconMiniEss.class) + public final ConfigChannel ess = new ConfigChannel<>("ess", this); + + @ChannelInfo(title = "GridMeter", description = "Sets the GridMeter nature.", type = FeneconMiniGridMeter.class) + public final ConfigChannel gridMeter = new ConfigChannel<>("gridMeter", this); + + @ChannelInfo(title = "ProductionMeter", description = "Sets the ProductionMeter nature.", type = FeneconMiniProductionMeter.class) + public final ConfigChannel productionMeter = new ConfigChannel<>("productionMeter", + this); + + @ChannelInfo(title = "ConsumptionMeter", description = "Sets the ConsumptionMeter nature.", type = FeneconMiniConsumptionMeter.class) + public final ConfigChannel consumptionMeter = new ConfigChannel<>("consumptionMeter", + this); + + /* + * Methods + */ + @Override + public String toString() { + return "FeneconMini [ess=" + ess + ", getThingId()=" + id() + "]"; + } + + @Override + protected Set getDeviceNatures() { + Set natures = new HashSet<>(); + if (ess.valueOptional().isPresent()) { + natures.add(ess.valueOptional().get()); + } + if (gridMeter.valueOptional().isPresent()) { + natures.add(gridMeter.valueOptional().get()); + } + if (productionMeter.valueOptional().isPresent()) { + natures.add(productionMeter.valueOptional().get()); + } + if (consumptionMeter.valueOptional().isPresent()) { + natures.add(consumptionMeter.valueOptional().get()); + } + return natures; + } +} diff --git a/edge/src/io/openems/impl/device/minireadonly/FeneconMiniConsumptionMeter.java b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniConsumptionMeter.java new file mode 100644 index 00000000000..f8abdcd134d --- /dev/null +++ b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniConsumptionMeter.java @@ -0,0 +1,103 @@ +package io.openems.impl.device.minireadonly; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.channel.ReadChannel; +import io.openems.api.channel.StaticValueChannel; +import io.openems.api.device.Device; +import io.openems.api.device.nature.meter.SymmetricMeterNature; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.ConfigException; +import io.openems.impl.protocol.modbus.ModbusDeviceNature; +import io.openems.impl.protocol.modbus.ModbusReadLongChannel; +import io.openems.impl.protocol.modbus.internal.ModbusProtocol; +import io.openems.impl.protocol.modbus.internal.UnsignedDoublewordElement; +import io.openems.impl.protocol.modbus.internal.UnsignedWordElement; +import io.openems.impl.protocol.modbus.internal.range.ModbusRegisterRange; + +@ThingInfo(title = "FENECON Mini Consumption-Meter") +public class FeneconMiniConsumptionMeter extends ModbusDeviceNature implements SymmetricMeterNature { + + /* + * Constructors + */ + public FeneconMiniConsumptionMeter(String thingId, Device parent) throws ConfigException { + super(thingId, parent); + } + + /* + * Config + */ + private final ConfigChannel type = new ConfigChannel("type", this).defaultValue("consumption"); + + @Override + public ConfigChannel type() { + return this.type; + } + + private final ConfigChannel maxActivePower = new ConfigChannel("maxActivePower", this); + + @Override + public ConfigChannel maxActivePower() { + return maxActivePower; + } + + private final ConfigChannel minActivePower = new ConfigChannel("minActivePower", this); + + @Override + public ConfigChannel minActivePower() { + return minActivePower; + } + + /* + * Inherited Channels + */ + private ModbusReadLongChannel activePower; + // Dummies + private StaticValueChannel reactivePower = new StaticValueChannel("ReactivePower", this, 0l); + + /* + * This Channels + */ + public ModbusReadLongChannel energy; + + @Override + public ReadChannel activePower() { + return this.activePower; + } + + @Override + public ReadChannel reactivePower() { + return this.reactivePower; + } + + @Override + public ReadChannel apparentPower() { + return this.activePower; + } + + @Override + public ReadChannel frequency() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ReadChannel voltage() { + // TODO Auto-generated method stub + return null; + } + + @Override + protected ModbusProtocol defineModbusProtocol() throws ConfigException { + ModbusProtocol protocol = new ModbusProtocol( // + new ModbusRegisterRange(5011, // + new UnsignedDoublewordElement(5011, // + this.energy = new ModbusReadLongChannel("Energy", this).unit("Wh").multiplier(2))), + new ModbusRegisterRange(4005, // + new UnsignedWordElement(4005, // + this.activePower = new ModbusReadLongChannel("ActivePower", this).unit("W") + .ignore(55536l)))); + return protocol; + } + +} diff --git a/edge/src/io/openems/impl/device/minireadonly/FeneconMiniEss.java b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniEss.java new file mode 100644 index 00000000000..45694805115 --- /dev/null +++ b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniEss.java @@ -0,0 +1,415 @@ +/******************************************************************************* + * OpenEMS - Open Source Energy Management System + * Copyright (c) 2016, 2017 FENECON GmbH and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Contributors: + * FENECON GmbH - initial API and implementation and initial documentation + *******************************************************************************/ +package io.openems.impl.device.minireadonly; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.channel.ReadChannel; +import io.openems.api.channel.StaticValueChannel; +import io.openems.api.channel.StatusBitChannels; +import io.openems.api.channel.WriteChannel; +import io.openems.api.device.Device; +import io.openems.api.device.nature.ess.AsymmetricEssNature; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.ConfigException; +import io.openems.impl.protocol.modbus.ModbusDeviceNature; +import io.openems.impl.protocol.modbus.ModbusReadLongChannel; +import io.openems.impl.protocol.modbus.internal.DummyElement; +import io.openems.impl.protocol.modbus.internal.ModbusProtocol; +import io.openems.impl.protocol.modbus.internal.UnsignedDoublewordElement; +import io.openems.impl.protocol.modbus.internal.UnsignedWordElement; +import io.openems.impl.protocol.modbus.internal.range.ModbusRegisterRange; + +@ThingInfo(title = "FENECON Mini ESS") +public class FeneconMiniEss extends ModbusDeviceNature implements AsymmetricEssNature { + + /* + * Constructors + */ + public FeneconMiniEss(String thingId, Device parent) throws ConfigException { + super(thingId, parent); + minSoc.addUpdateListener((channel, newValue) -> { + // If chargeSoc was not set -> set it to minSoc minus 2 + if (channel == minSoc && !chargeSoc.valueOptional().isPresent()) { + chargeSoc.updateValue((Integer) newValue.get() - 2, false); + } + }); + } + + /* + * Config + */ + private ConfigChannel minSoc = new ConfigChannel("minSoc", this).defaultValue(0); + private ConfigChannel chargeSoc = new ConfigChannel("chargeSoc", this).defaultValue(0); + + @Override + public ConfigChannel minSoc() { + return minSoc; + } + + @Override + public ConfigChannel chargeSoc() { + return chargeSoc; + } + + /* + * Inherited Channels + */ + private ModbusReadLongChannel soc; + private ModbusReadLongChannel activePowerL1; + private ModbusReadLongChannel activePowerL3; + private ModbusReadLongChannel activePowerL2; + private ModbusReadLongChannel systemState; + // Dummies + private StaticValueChannel allowedCharge = new StaticValueChannel("AllowedCharge", this, 0l); + private StaticValueChannel allowedDischarge = new StaticValueChannel("AllowedDischarge", this, 0l); + private StaticValueChannel allowedApparent = new StaticValueChannel("AllowedApparent", this, 0l); + private StaticValueChannel gridMode = new StaticValueChannel("GridMode", this, 0l); + private StaticValueChannel reactivePowerL1 = new StaticValueChannel("ReactivePowerL1", this, 0l); + private StaticValueChannel reactivePowerL2 = new StaticValueChannel("ReactivePowerL2", this, 0l); + private StaticValueChannel reactivePowerL3 = new StaticValueChannel("ReactivePowerL3", this, 0l); + private StatusBitChannels warning = new StatusBitChannels("Warning", this); + + /* + * This channels + */ + public ModbusReadLongChannel operatingMode; + public ModbusReadLongChannel controlMode; + public ModbusReadLongChannel allowedChargeEnergy; + public ModbusReadLongChannel dischargedEnergy; + public ModbusReadLongChannel batteryGroupStatus; + public ModbusReadLongChannel becu1ChargeCurr; + public ModbusReadLongChannel becu1DischargeCurr; + public ModbusReadLongChannel becu1Volt; + public ModbusReadLongChannel becu1Curr; + public ModbusReadLongChannel becu1Soc; + public ModbusReadLongChannel becu1Alarm1; + public ModbusReadLongChannel becu1Alarm2; + public ModbusReadLongChannel becu1Fault1; + public ModbusReadLongChannel becu1Fault2; + public ModbusReadLongChannel becu1Version; + public ModbusReadLongChannel becu1MinVoltNo; + public ModbusReadLongChannel becu1MinVolt; + public ModbusReadLongChannel becu1MaxVoltNo; + public ModbusReadLongChannel becu1MaxVolt; + public ModbusReadLongChannel becu1MinTempNo; + public ModbusReadLongChannel becu1MinTemp; + public ModbusReadLongChannel becu1MaxTempNo; + public ModbusReadLongChannel becu1MaxTemp; + public ModbusReadLongChannel becu2ChargeCurr; + public ModbusReadLongChannel becu2DischargeCurr; + public ModbusReadLongChannel becu2Volt; + public ModbusReadLongChannel becu2Curr; + public ModbusReadLongChannel becu2Soc; + public ModbusReadLongChannel becu2Alarm1; + public ModbusReadLongChannel becu2Alarm2; + public ModbusReadLongChannel becu2Fault1; + public ModbusReadLongChannel becu2Fault2; + public ModbusReadLongChannel becu2Version; + public ModbusReadLongChannel becu2MinVoltNo; + public ModbusReadLongChannel becu2MinVolt; + public ModbusReadLongChannel becu2MaxVoltNo; + public ModbusReadLongChannel becu2MaxVolt; + public ModbusReadLongChannel becu2MinTempNo; + public ModbusReadLongChannel becu2MinTemp; + public ModbusReadLongChannel becu2MaxTempNo; + public ModbusReadLongChannel becu2MaxTemp; + public ModbusReadLongChannel systemWorkState; + public ModbusReadLongChannel systemWorkModeState; + public ModbusReadLongChannel becuNum; + public ModbusReadLongChannel becuWorkState; + public ModbusReadLongChannel becuChargeCurr; + public ModbusReadLongChannel becuDischargeCurr; + public ModbusReadLongChannel becuVolt; + public ModbusReadLongChannel becuCurr; + public ModbusReadLongChannel becuFault1; + public ModbusReadLongChannel becuFault2; + public ModbusReadLongChannel becuAlarm1; + public ModbusReadLongChannel becuAlarm2; + public ModbusReadLongChannel batteryGroupSoc; + public ModbusReadLongChannel batteryGroupVoltage; + public ModbusReadLongChannel batteryGroupCurr; + public ModbusReadLongChannel batteryGroupPower; + + @Override + public ReadChannel allowedCharge() { + return allowedCharge; + } + + @Override + public ReadChannel allowedDischarge() { + return allowedDischarge; + } + + @Override + public ReadChannel gridMode() { + return gridMode; + } + + @Override + public ReadChannel soc() { + return soc; + } + + @Override + public ReadChannel systemState() { + return systemState; + } + + @Override + public ReadChannel activePowerL1() { + return activePowerL1; + } + + @Override + public ReadChannel activePowerL2() { + return activePowerL2; + } + + @Override + public ReadChannel activePowerL3() { + return activePowerL3; + } + + @Override + public ReadChannel allowedApparent() { + return this.allowedApparent; + } + + @Override + public ReadChannel capacity() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ReadChannel maxNominalPower() { + // TODO Auto-generated method stub + return null; + } + + @Override + public StatusBitChannels warning() { + return this.warning; + } + + @Override + public ReadChannel reactivePowerL1() { + return this.reactivePowerL1; + } + + @Override + public ReadChannel reactivePowerL2() { + return this.reactivePowerL2; + } + + @Override + public ReadChannel reactivePowerL3() { + return this.reactivePowerL3; + } + + @Override + public WriteChannel setActivePowerL1() { + // TODO Auto-generated method stub + return null; + } + + @Override + public WriteChannel setActivePowerL2() { + // TODO Auto-generated method stub + return null; + } + + @Override + public WriteChannel setActivePowerL3() { + // TODO Auto-generated method stub + return null; + } + + @Override + public WriteChannel setReactivePowerL1() { + // TODO Auto-generated method stub + return null; + } + + @Override + public WriteChannel setReactivePowerL2() { + // TODO Auto-generated method stub + return null; + } + + @Override + public WriteChannel setReactivePowerL3() { + // TODO Auto-generated method stub + return null; + } + + @Override + public WriteChannel setWorkState() { + // TODO Auto-generated method stub + return null; + } + + /* + * Methods + */ + @Override + protected ModbusProtocol defineModbusProtocol() throws ConfigException { + ModbusProtocol protocol = new ModbusProtocol( // + new ModbusRegisterRange(100, // + new UnsignedWordElement(100, // + this.systemState = new ModbusReadLongChannel("SystemState", this)), + new UnsignedWordElement(101, // + this.controlMode = new ModbusReadLongChannel("ControlMode", this)), + new DummyElement(102, 103), new UnsignedDoublewordElement(104, // + this.allowedChargeEnergy = new ModbusReadLongChannel("BatteryAllowedCharging", this)), + new UnsignedDoublewordElement(106, // + this.dischargedEnergy = new ModbusReadLongChannel("DischargedEnergy", this)), + new UnsignedWordElement(108, // + this.batteryGroupStatus = new ModbusReadLongChannel("BatteryGroupStatus", this)), + new UnsignedWordElement(109, // + this.batteryGroupSoc = new ModbusReadLongChannel("BatteryGroupSoc", this)), + new UnsignedWordElement(110, // + this.batteryGroupVoltage = new ModbusReadLongChannel("BatteryGroupVoltage", this)), + new UnsignedWordElement(111, // + this.batteryGroupCurr = new ModbusReadLongChannel("BatteryGroupCurr", this)), + new UnsignedWordElement(112, // + this.batteryGroupPower = new ModbusReadLongChannel("BatteryGroupPower", this))), + new ModbusRegisterRange(2007, // + new UnsignedWordElement(2007, // + this.activePowerL1 = new ModbusReadLongChannel("ActivePowerL1", this).unit("W") + .delta(10000l))), + new ModbusRegisterRange(2107, // + new UnsignedWordElement(2107, // + this.activePowerL2 = new ModbusReadLongChannel("ActivePowerL2", this).unit("W") + .delta(10000l))), + new ModbusRegisterRange(2207, // + new UnsignedWordElement(2207, // + this.activePowerL3 = new ModbusReadLongChannel("ActivePowerL3", this).unit("W") + .delta(10000l))), + new ModbusRegisterRange(3000, // + new UnsignedWordElement(3000, // + this.becu1ChargeCurr = new ModbusReadLongChannel("Becu1ChargeCurr", this)), + new UnsignedWordElement(3001, // + this.becu1DischargeCurr = new ModbusReadLongChannel("Becu1DischargeCurr", this)), + new UnsignedWordElement(3002, // + this.becu1Volt = new ModbusReadLongChannel("Becu1Volt", this)), + new UnsignedWordElement(3003, // + this.becu1Curr = new ModbusReadLongChannel("Becu1Curr", this)), + new UnsignedWordElement(3004, // + this.becu1Soc = new ModbusReadLongChannel("Becu1Soc", this)), + new UnsignedWordElement(3005, // + this.becu1Alarm1 = new ModbusReadLongChannel("Becu1Alarm1", this)), + new UnsignedWordElement(3006, // + this.becu1Alarm2 = new ModbusReadLongChannel("Becu1Alarm2", this)), + new UnsignedWordElement(3007, // + this.becu1Fault1 = new ModbusReadLongChannel("Becu1Fault1", this)), + new UnsignedWordElement(3008, // + this.becu1Fault2 = new ModbusReadLongChannel("Becu1Fault2", this)), + new UnsignedWordElement(3009, // + this.becu1Version = new ModbusReadLongChannel("Becu1Version", this)), + new DummyElement(3010, 3011), // + new UnsignedWordElement(3012, // + this.becu1MinVoltNo = new ModbusReadLongChannel("Becu1MinVoltNo", this)), + new UnsignedWordElement(3013, // + this.becu1MinVolt = new ModbusReadLongChannel("Becu1MinVolt", this)), + new UnsignedWordElement(3014, // + this.becu1MaxVoltNo = new ModbusReadLongChannel("Becu1MaxVoltNo", this)), + new UnsignedWordElement(3015, // + this.becu1MaxVolt = new ModbusReadLongChannel("Becu1MaxVolt", this)), + new UnsignedWordElement(3016, // + this.becu1MinTempNo = new ModbusReadLongChannel("Becu1MinTempNo", this)), + new UnsignedWordElement(3017, // + this.becu1MinTemp = new ModbusReadLongChannel("Becu1MinTemp", this)), + new UnsignedWordElement(3018, // + this.becu1MaxTempNo = new ModbusReadLongChannel("Becu1MaxTempNo", this)), + new UnsignedWordElement(3019, // + this.becu1MaxTemp = new ModbusReadLongChannel("Becu1MaxTemp", this))), + new ModbusRegisterRange(3200, // + new UnsignedWordElement(3200, // + this.becu2ChargeCurr = new ModbusReadLongChannel("Becu2ChargeCurr", this)), + new UnsignedWordElement(3201, // + this.becu2DischargeCurr = new ModbusReadLongChannel("Becu2DischargeCurr", this)), + new UnsignedWordElement(3202, // + this.becu2Volt = new ModbusReadLongChannel("Becu2Volt", this)), + new UnsignedWordElement(3203, // + this.becu2Curr = new ModbusReadLongChannel("Becu2Curr", this)), + new UnsignedWordElement(3204, // + this.becu2Soc = new ModbusReadLongChannel("Becu2Soc", this)), + new UnsignedWordElement(3205, // + this.becu2Alarm1 = new ModbusReadLongChannel("Becu2Alarm1", this)), + new UnsignedWordElement(3206, // + this.becu2Alarm2 = new ModbusReadLongChannel("Becu2Alarm2", this)), + new UnsignedWordElement(3207, // + this.becu2Fault1 = new ModbusReadLongChannel("Becu2Fault1", this)), + new UnsignedWordElement(3208, // + this.becu2Fault2 = new ModbusReadLongChannel("Becu2Fault2", this)), + new UnsignedWordElement(3209, // + this.becu2Version = new ModbusReadLongChannel("Becu2Version", this)), + new DummyElement(3210, 3211), // + new UnsignedWordElement(3212, // + this.becu2MinVoltNo = new ModbusReadLongChannel("Becu2MinVoltNo", this)), + new UnsignedWordElement(3213, // + this.becu2MinVolt = new ModbusReadLongChannel("Becu2MinVolt", this)), + new UnsignedWordElement(3214, // + this.becu2MaxVoltNo = new ModbusReadLongChannel("Becu2MaxVoltNo", this)), + new UnsignedWordElement(3215, // + this.becu2MaxVolt = new ModbusReadLongChannel("Becu2MaxVolt", this)), + new UnsignedWordElement(3216, // + this.becu2MinTempNo = new ModbusReadLongChannel("Becu2MinTempNo", this)), + new UnsignedWordElement(3217, // + this.becu2MinTemp = new ModbusReadLongChannel("Becu2MinTemp", this)), + new UnsignedWordElement(3218, // + this.becu2MaxTempNo = new ModbusReadLongChannel("Becu2MaxTempNo", this)), + new UnsignedWordElement(3219, // + this.becu2MaxTemp = new ModbusReadLongChannel("Becu2MaxTemp", this))), + new ModbusRegisterRange(4000, // + new UnsignedWordElement(4000, // + this.systemWorkState = new ModbusReadLongChannel("SystemWorkState", this)), + new UnsignedWordElement(4001, // + this.systemWorkModeState = new ModbusReadLongChannel("SystemWorkModeState", this))), + new ModbusRegisterRange(4800, // + new UnsignedWordElement(4800, // + this.becuNum = new ModbusReadLongChannel("BecuNum", this)), + new UnsignedWordElement(4801, // + this.becuWorkState = new ModbusReadLongChannel("BecuWorkState", this)), + new DummyElement(4802, 4802), new UnsignedWordElement(4803, // + this.becuChargeCurr = new ModbusReadLongChannel("BecuChargeCurr", this)), + new UnsignedWordElement(4804, // + this.becuDischargeCurr = new ModbusReadLongChannel("BecuDischargeCurr", this)), + new UnsignedWordElement(4805, // + this.becuVolt = new ModbusReadLongChannel("BecuVolt", this)), + new UnsignedWordElement(4806, // + this.becuCurr = new ModbusReadLongChannel("BecuCurr", this)), + new UnsignedWordElement(4807, // + this.becuWorkState = new ModbusReadLongChannel("BecuWorkState", this)), + new UnsignedWordElement(4808, // + this.becuFault1 = new ModbusReadLongChannel("BecuFault1", this)), + new UnsignedWordElement(4809, // + this.becuFault2 = new ModbusReadLongChannel("BecuFault2", this)), + new UnsignedWordElement(4810, // + this.becuAlarm1 = new ModbusReadLongChannel("BecuAlarm1", this)), + new UnsignedWordElement(4811, // + this.becuAlarm2 = new ModbusReadLongChannel("BecuAlarm2", this)), + new UnsignedWordElement(4812, // + this.soc = new ModbusReadLongChannel("Soc", this).unit("%")))); + return protocol; + } +} diff --git a/edge/src/io/openems/impl/device/minireadonly/FeneconMiniGridMeter.java b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniGridMeter.java new file mode 100644 index 00000000000..55eb43e0937 --- /dev/null +++ b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniGridMeter.java @@ -0,0 +1,108 @@ +package io.openems.impl.device.minireadonly; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.channel.ReadChannel; +import io.openems.api.channel.StaticValueChannel; +import io.openems.api.device.Device; +import io.openems.api.device.nature.meter.SymmetricMeterNature; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.ConfigException; +import io.openems.impl.protocol.modbus.ModbusDeviceNature; +import io.openems.impl.protocol.modbus.ModbusReadLongChannel; +import io.openems.impl.protocol.modbus.internal.ModbusProtocol; +import io.openems.impl.protocol.modbus.internal.SignedWordElement; +import io.openems.impl.protocol.modbus.internal.UnsignedDoublewordElement; +import io.openems.impl.protocol.modbus.internal.range.ModbusRegisterRange; + +@ThingInfo(title = "FENECON Mini Grid-Meter") +public class FeneconMiniGridMeter extends ModbusDeviceNature implements SymmetricMeterNature { + + /* + * Constructors + */ + public FeneconMiniGridMeter(String thingId, Device parent) throws ConfigException { + super(thingId, parent); + } + + /* + * Config + */ + private final ConfigChannel type = new ConfigChannel("type", this).defaultValue("grid"); + + @Override + public ConfigChannel type() { + return this.type; + } + + private final ConfigChannel maxActivePower = new ConfigChannel("maxActivePower", this); + + @Override + public ConfigChannel maxActivePower() { + return maxActivePower; + } + + private final ConfigChannel minActivePower = new ConfigChannel("minActivePower", this); + + @Override + public ConfigChannel minActivePower() { + return minActivePower; + } + + /* + * Inherited Channels + */ + private ModbusReadLongChannel activePower; + // Dummies + private StaticValueChannel reactivePower = new StaticValueChannel("ReactivePower", this, 0l); + + /* + * This Channels + */ + public ModbusReadLongChannel buyFromGridEnergy; + public ModbusReadLongChannel sellToGridEnergy; + + @Override + public ReadChannel activePower() { + return this.activePower; + } + + @Override + public ReadChannel apparentPower() { + return this.activePower; + } + + @Override + public ReadChannel reactivePower() { + return this.reactivePower; + } + + @Override + public ReadChannel frequency() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ReadChannel voltage() { + // TODO Auto-generated method stub + return null; + } + + @Override + protected ModbusProtocol defineModbusProtocol() throws ConfigException { + ModbusProtocol protocol = new ModbusProtocol( // + new ModbusRegisterRange(4004, // + new SignedWordElement(4004, // + this.activePower = new ModbusReadLongChannel("ActivePower", this).unit("W").negate())), + new ModbusRegisterRange(5003, // + new UnsignedDoublewordElement(5003, // + this.sellToGridEnergy = new ModbusReadLongChannel("SellToGridEnergy", this).unit("Wh") + .multiplier(2)), + new UnsignedDoublewordElement(5005, // + this.buyFromGridEnergy = new ModbusReadLongChannel("BuyFromGridEnergy", this).unit("Wh") + .multiplier(2)))); + + return protocol; + } + +} diff --git a/edge/src/io/openems/impl/device/minireadonly/FeneconMiniProductionMeter.java b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniProductionMeter.java new file mode 100644 index 00000000000..2bccf623868 --- /dev/null +++ b/edge/src/io/openems/impl/device/minireadonly/FeneconMiniProductionMeter.java @@ -0,0 +1,102 @@ +package io.openems.impl.device.minireadonly; + +import io.openems.api.channel.ConfigChannel; +import io.openems.api.channel.ReadChannel; +import io.openems.api.channel.StaticValueChannel; +import io.openems.api.device.Device; +import io.openems.api.device.nature.meter.SymmetricMeterNature; +import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.ConfigException; +import io.openems.impl.protocol.modbus.ModbusDeviceNature; +import io.openems.impl.protocol.modbus.ModbusReadLongChannel; +import io.openems.impl.protocol.modbus.internal.ModbusProtocol; +import io.openems.impl.protocol.modbus.internal.UnsignedDoublewordElement; +import io.openems.impl.protocol.modbus.internal.UnsignedWordElement; +import io.openems.impl.protocol.modbus.internal.range.ModbusRegisterRange; + +@ThingInfo(title = "FENECON Mini Production-Meter") +public class FeneconMiniProductionMeter extends ModbusDeviceNature implements SymmetricMeterNature { + + /* + * Constructors + */ + public FeneconMiniProductionMeter(String thingId, Device parent) throws ConfigException { + super(thingId, parent); + } + + /* + * Config + */ + private final ConfigChannel type = new ConfigChannel("type", this).defaultValue("production"); + + @Override + public ConfigChannel type() { + return this.type; + } + + private final ConfigChannel maxActivePower = new ConfigChannel("maxActivePower", this); + + @Override + public ConfigChannel maxActivePower() { + return maxActivePower; + } + + private final ConfigChannel minActivePower = new ConfigChannel("minActivePower", this); + + @Override + public ConfigChannel minActivePower() { + return minActivePower; + } + + /* + * Inherited Channels + */ + private ModbusReadLongChannel activePower; + // Dummies + private StaticValueChannel reactivePower = new StaticValueChannel("ReactivePower", this, 0l); + + /* + * This Channels + */ + public ModbusReadLongChannel energy; + + @Override + public ReadChannel activePower() { + return this.activePower; + } + + @Override + public ReadChannel reactivePower() { + return this.reactivePower; + } + + @Override + public ReadChannel apparentPower() { + return this.activePower; + } + + @Override + public ReadChannel frequency() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ReadChannel voltage() { + // TODO Auto-generated method stub + return null; + } + + @Override + protected ModbusProtocol defineModbusProtocol() throws ConfigException { + ModbusProtocol protocol = new ModbusProtocol( // + new ModbusRegisterRange(4006, /// + new UnsignedWordElement(4006, // + this.activePower = new ModbusReadLongChannel("ActivePower", this).unit("W"))), + new ModbusRegisterRange(4036, /// + new UnsignedDoublewordElement(4036, // + this.energy = new ModbusReadLongChannel("Energy", this).unit("Wh")))); + return protocol; + } + +} diff --git a/edge/src/io/openems/impl/persistence/fenecon/FeneconPersistence.java b/edge/src/io/openems/impl/persistence/fenecon/FeneconPersistence.java index 61efe424db0..e02a971083e 100644 --- a/edge/src/io/openems/impl/persistence/fenecon/FeneconPersistence.java +++ b/edge/src/io/openems/impl/persistence/fenecon/FeneconPersistence.java @@ -42,6 +42,8 @@ import io.openems.api.device.nature.DeviceNature; import io.openems.api.doc.ChannelInfo; import io.openems.api.doc.ThingInfo; +import io.openems.api.exception.ConfigException; +import io.openems.api.exception.NotImplementedException; import io.openems.api.persistence.Persistence; import io.openems.api.thing.Thing; import io.openems.common.types.FieldValue; @@ -49,6 +51,9 @@ import io.openems.common.types.NumberFieldValue; import io.openems.common.types.StringFieldValue; import io.openems.common.websocket.DefaultMessages; +import io.openems.common.websocket.WebSocketUtils; +import io.openems.core.Config; +import io.openems.core.ConfigFormat; import io.openems.core.Databus; import io.openems.core.ThingRepository; import io.openems.core.utilities.websocket.EdgeWebsocketHandler; @@ -57,6 +62,8 @@ @ThingInfo(title = "FENECON Persistence", description = "Establishes the connection to FENECON Cloud.") public class FeneconPersistence extends Persistence implements ChannelChangeListener { + private final static String DEFAULT_CONFIG_LANGUAGE = "en"; + /* * Config */ @@ -75,13 +82,23 @@ public class FeneconPersistence extends Persistence implements ChannelChangeList */ public FeneconPersistence() { this.websocketHandler = new EdgeWebsocketHandler(); - this.reconnectingWebsocket = new ReconnectingWebsocket(this.websocketHandler, () -> { + this.reconnectingWebsocket = new ReconnectingWebsocket(this.websocketHandler, (websocket) -> { /* * onOpen */ log.info("FENECON persistence connected [" + uri.valueOptional().orElse("") + "]"); // Add current status of all channels to queue this.addCurrentValueOfAllChannelsToQueue(); + // Send current config + try { + WebSocketUtils.send( // + websocket, // + DefaultMessages.configQueryReply( + Config.getInstance().getJson(ConfigFormat.OPENEMS_UI, DEFAULT_CONFIG_LANGUAGE))); + log.info("Sent config to FENECON persistence."); + } catch (NotImplementedException | ConfigException e) { + log.error("Unable to send config: " + e.getMessage()); + } }, () -> { /* * onClose diff --git a/edge/src/io/openems/impl/persistence/fenecon/ReconnectingWebsocket.java b/edge/src/io/openems/impl/persistence/fenecon/ReconnectingWebsocket.java index a2f21b4f8cf..eb34f6f524b 100644 --- a/edge/src/io/openems/impl/persistence/fenecon/ReconnectingWebsocket.java +++ b/edge/src/io/openems/impl/persistence/fenecon/ReconnectingWebsocket.java @@ -31,6 +31,9 @@ public class ReconnectingWebsocket { private final Logger log = LoggerFactory.getLogger(ReconnectingWebsocket.class); + private final int DEFAULT_WAIT_AFTER_CLOSE = 1; // 1 second + private final int MAX_WAIT_AFTER_CLOSE = 60 * 3; // 3 minutes + private int WAIT_AFTER_CLOSE = DEFAULT_WAIT_AFTER_CLOSE; private final Draft WEBSOCKET_DRAFT = new Draft_6455(); private final EdgeWebsocketHandler WEBSOCKET_HANDLER; private final Mutex WEBSOCKET_CLOSED = new Mutex(true); @@ -42,6 +45,8 @@ public class ReconnectingWebsocket { private final ScheduledExecutorService reconnectorExecutor = Executors .newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("Re-Ws-%d").build()); + private final ScheduledExecutorService waitAfterCloseExecutor = Executors + .newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("Re-WC-%d").build()); private ScheduledFuture reconnectorFuture = null; private final Runnable reconnectorTask; @@ -56,12 +61,15 @@ public MyWebSocketClient(URI uri, Map httpHeaders) throws IOExce if (uri.getScheme().toString().equals("wss")) { this.setSocket(SSLSocketFactory.getDefault().createSocket()); } + log.info("I was built. ID [" + Thread.currentThread().getId() + "] name [" + + Thread.currentThread().getName() + "]"); } @Override public void onOpen(ServerHandshake handshakedata) { log.info("Websocket [" + this.getURI().toString() + "] opened"); - ON_OPEN_LISTENER.announce(); + ON_OPEN_LISTENER.announce(this); + WAIT_AFTER_CLOSE = DEFAULT_WAIT_AFTER_CLOSE; } @Override @@ -70,15 +78,23 @@ public void onMessage(String message) { JsonObject jMessage = (new JsonParser()).parse(message).getAsJsonObject(); WEBSOCKET_HANDLER.onMessage(jMessage); } catch (Throwable t) { - log.error("Websocket [" + this.getURI().toString() + "] error on message [" + message + "]"); + log.error("Websocket [" + this.getURI().toString() + "] error on message [" + message + "]: " + + t.getMessage()); + t.printStackTrace(); } } @Override public void onClose(int code, String reason, boolean remote) { - log.info( - "Websocket [" + this.getURI().toString() + "] closed. Code [" + code + "] Reason [" + reason + "]"); - WEBSOCKET_CLOSED.release(); // trigger reconnector + log.info("Websocket [" + this.getURI().toString() + "] closed. Code [" + code + "] Reason [" + reason + + "] Wait [" + WAIT_AFTER_CLOSE + "]"); + WAIT_AFTER_CLOSE += DEFAULT_WAIT_AFTER_CLOSE; + if (WAIT_AFTER_CLOSE > MAX_WAIT_AFTER_CLOSE) { + WAIT_AFTER_CLOSE = MAX_WAIT_AFTER_CLOSE; + } + waitAfterCloseExecutor.schedule(() -> { + WEBSOCKET_CLOSED.release(); // trigger reconnector + }, WAIT_AFTER_CLOSE, TimeUnit.SECONDS); ON_CLOSE_LISTENER.announce(); } @@ -86,11 +102,18 @@ public void onClose(int code, String reason, boolean remote) { public void onError(Exception ex) { log.warn("Websocket [" + this.getURI().toString() + "] error: " + ex.getMessage()); } + + @Override + protected void finalize() throws Throwable { + System.out.println("Finalize... [" + Thread.currentThread().getId() + "] name [" + + Thread.currentThread().getName() + "]"); + super.finalize(); + } } @FunctionalInterface public interface OnOpenListener { - public void announce(); + public void announce(WebSocket websocket); } @FunctionalInterface @@ -112,8 +135,8 @@ public ReconnectingWebsocket(EdgeWebsocketHandler handler, OnOpenListener onOpen this.reconnectorTask = () -> { while (true) { try { - // wait for websocket close or check once in a minute - WEBSOCKET_CLOSED.awaitOrTimeout(1, TimeUnit.MINUTES); + // wait for websocket close or check once every 5 minutes + WEBSOCKET_CLOSED.awaitOrTimeout(5, TimeUnit.MINUTES); if (WEBSOCKET_OPT.isPresent()) { WebSocket ws = WEBSOCKET_OPT.get(); diff --git a/edge/src/io/openems/impl/protocol/modbus/ModbusReadLongChannel.java b/edge/src/io/openems/impl/protocol/modbus/ModbusReadLongChannel.java index c6f4e695611..b7c3e6b102d 100644 --- a/edge/src/io/openems/impl/protocol/modbus/ModbusReadLongChannel.java +++ b/edge/src/io/openems/impl/protocol/modbus/ModbusReadLongChannel.java @@ -57,6 +57,11 @@ public ModbusReadLongChannel delta(Long delta) { return (ModbusReadLongChannel) super.delta(delta); } + @Override + public ModbusReadLongChannel ignore(Long value) { + return (ModbusReadLongChannel) super.ignore(value); + } + @Override public ModbusReadLongChannel interval(Long min, Long max) { return (ModbusReadLongChannel) super.interval(min, max); diff --git a/edge/template/FeneconMini.json b/edge/template/FeneconMini.json new file mode 100644 index 00000000000..04c255848ac --- /dev/null +++ b/edge/template/FeneconMini.json @@ -0,0 +1,60 @@ +{ + "things": [ + { + "class": "io.openems.impl.protocol.modbus.ModbusRtu", + "serialinterface": "/dev/ttyUSB0", + "baudrate": 9600, + "databits": 8, + "parity": "none", + "stopbits": 1, + "devices": [ + { + "class": "io.openems.impl.device.minireadonly.FeneconMini", + "modbusUnitId": 4, + "ess": { + "id": "ess0", + "minSoc": 15 + }, + "gridMeter": { + "id": "meter0" + }, + "productionMeter": { + "id": "meter1" + }, + "consumptionMeter": { + "id": "meter2" + } + } + ] + } + ], + "scheduler": { + "class": "io.openems.impl.scheduler.SimpleScheduler", + "cycleTime": 10000, + "controllers": [ + { + "priority": 150, + "class": "io.openems.impl.controller.debuglog.DebugLogController", + "esss": [ + "ess0" + ], + "meters": [ + "meter0", + "meter1", + "meter2" + ] + } + ] + }, + "persistence": [ + { + "class": "io.openems.impl.persistence.influxdb.InfluxdbPersistence", + "ip": "127.0.0.1", + "fems": "###FEMS###" + }, + { + "class": "io.openems.impl.persistence.fenecon.FeneconPersistence", + "apikey": "###APIKEY###" + } + ] +} diff --git a/edge/template/persistence/openems-backend-dev.json b/edge/template/persistence/openems-backend-dev.json new file mode 100644 index 00000000000..edd8cc28a3a --- /dev/null +++ b/edge/template/persistence/openems-backend-dev.json @@ -0,0 +1,9 @@ +{ + "persistence": [ + { + "class": "io.openems.impl.persistence.fenecon.FeneconPersistence", + "apikey": "", + "uri": "wss://fenecon.de:443/openems-backend-dev" + } + ] +} diff --git a/edge/template/persistence/openems-backend.json b/edge/template/persistence/openems-backend.json new file mode 100644 index 00000000000..8551d81a3bd --- /dev/null +++ b/edge/template/persistence/openems-backend.json @@ -0,0 +1,8 @@ +{ + "persistence": [ + { + "class": "io.openems.impl.persistence.fenecon.FeneconPersistence", + "apikey": "" + } + ] +} diff --git a/pom.xml b/pom.xml index fdec138c0a7..977ba9e76f3 100644 --- a/pom.xml +++ b/pom.xml @@ -48,12 +48,6 @@ influxdb-java ${influxdb.version} - - junit - junit - ${junit.version} - test - org.slf4j slf4j-api diff --git a/ui/.angular-cli.json b/ui/.angular-cli.json index 297c2639659..30f6af70024 100644 --- a/ui/.angular-cli.json +++ b/ui/.angular-cli.json @@ -29,7 +29,8 @@ "environmentSource": "environments/environment.ts", "environments": { "backend": "environments/openems-backend.ts", - "backend-dev": "environments/openems-backend-dev.ts", + "backend-dev-local": "environments/openems-backend-dev-local.ts", + "backend-dev-live": "environments/openems-backend-dev-live.ts", "edge": "environments/openems-edge.ts", "dev": "environments/environment.ts" } @@ -60,4 +61,4 @@ "styleExt": "css", "component": {} } -} +} \ No newline at end of file diff --git a/ui/src/app/device/history/chart/energychart/energychart.component.ts b/ui/src/app/device/history/chart/energychart/energychart.component.ts index 6d9e8d0d04f..f907188fecd 100644 --- a/ui/src/app/device/history/chart/energychart/energychart.component.ts +++ b/ui/src/app/device/history/chart/energychart/energychart.component.ts @@ -46,14 +46,29 @@ export class EnergyChartComponent implements OnChanges { private gridSell: String = ""; private colors = [{ + // Production backgroundColor: 'rgba(45,143,171,0.2)', borderColor: 'rgba(45,143,171,1)', }, { + // Grid Buy backgroundColor: 'rgba(0,0,0,0.2)', borderColor: 'rgba(0,0,0,1)', }, { + // Grid Sell + backgroundColor: 'rgba(0,0,200,0.2)', + borderColor: 'rgba(0,0,200,1)', + }, { + // Consumption backgroundColor: 'rgba(221,223,1,0.2)', borderColor: 'rgba(221,223,1,1)', + }, { + // Storage Charge + backgroundColor: 'rgba(0,223,0,0.2)', + borderColor: 'rgba(0,223,0,1)', + }, { + // Storage Discharge + backgroundColor: 'rgba(200,0,0,0.2)', + borderColor: 'rgba(200,0,0,1)', }]; private options: ChartOptions; @@ -84,26 +99,47 @@ export class EnergyChartComponent implements OnChanges { // prepare datasets and labels let activePowers = { production: [], - grid: [], - consumption: [] + gridBuy: [], + gridSell: [], + consumption: [], + storageCharge: [], + storageDischarge: [] } let labels: moment.Moment[] = []; for (let record of historicData.data) { labels.push(moment(record.time)); let data = new CurrentDataAndSummary(record.channels, this.config); - activePowers.grid.push(Utils.divideSafely(data.summary.grid.activePower, -1000)); // convert to kW and invert value + activePowers.gridBuy.push(Utils.divideSafely(data.summary.grid.buyActivePower, 1000)); // convert to kW + activePowers.gridSell.push(Utils.divideSafely(data.summary.grid.sellActivePower, 1000)); // convert to kW activePowers.production.push(Utils.divideSafely(data.summary.production.activePower, 1000)); // convert to kW activePowers.consumption.push(Utils.divideSafely(data.summary.consumption.activePower, 1000)); // convert to kW + activePowers.storageCharge.push(Utils.divideSafely(data.summary.storage.chargeActivePower, 1000)); // convert to kW + activePowers.storageDischarge.push(Utils.divideSafely(data.summary.storage.dischargeActivePower, 1000)); // convert to kW } this.datasets = [{ label: this.translate.instant('General.Production'), - data: activePowers.production + data: activePowers.production, + hidden: false }, { - label: this.translate.instant('General.Grid'), - data: activePowers.grid + label: "Netzbezug", //TODO translate this.translate.instant('General.Grid') + data: activePowers.gridBuy, + hidden: false + }, { + label: "Netzeinspeisung", // TODO translate + data: activePowers.gridSell, + hidden: false }, { label: this.translate.instant('General.Consumption'), - data: activePowers.consumption + data: activePowers.consumption, + hidden: false + }, { + label: "Speicher-Beladung", // TODO translate + data: activePowers.storageCharge, + hidden: true + }, { + label: "Speicher-Entladung", // TODO translate + data: activePowers.storageDischarge, + hidden: true }]; this.labels = labels; // stop loading spinner diff --git a/ui/src/app/device/overview/energymonitor/chart/chart.component.ts b/ui/src/app/device/overview/energymonitor/chart/chart.component.ts index d35d73bbdee..b583021969e 100644 --- a/ui/src/app/device/overview/energymonitor/chart/chart.component.ts +++ b/ui/src/app/device/overview/energymonitor/chart/chart.component.ts @@ -68,13 +68,13 @@ export class EnergymonitorChartComponent implements OnInit, OnDestroy { * Set values for energy monitor */ let summary = currentData.summary; - this.storageSection.updateValue(summary.storage.activePower, summary.storage.soc); - this.gridSection.updateValue(summary.grid.activePower, summary.grid.powerRatio); + this.storageSection.updateStorageValue(summary.storage.chargeActivePower, summary.storage.dischargeActivePower, summary.storage.soc); + this.gridSection.updateGridValue(summary.grid.buyActivePower, summary.grid.sellActivePower, summary.grid.powerRatio); this.consumptionSection.updateValue(Math.round(summary.consumption.activePower), Math.round(summary.consumption.powerRatio)); this.productionSection.updateValue(summary.production.activePower, summary.production.powerRatio); } else { - this.storageSection.updateValue(null, null); - this.gridSection.updateValue(null, null); + this.storageSection.updateStorageValue(null, null, null); + this.gridSection.updateGridValue(null, null, null); this.consumptionSection.updateValue(null, null); this.productionSection.updateValue(null, null); } diff --git a/ui/src/app/device/overview/energymonitor/chart/section/abstractsection.component.ts b/ui/src/app/device/overview/energymonitor/chart/section/abstractsection.component.ts index 00b08f9d15f..bb1607a2e15 100644 --- a/ui/src/app/device/overview/energymonitor/chart/section/abstractsection.component.ts +++ b/ui/src/app/device/overview/energymonitor/chart/section/abstractsection.component.ts @@ -73,8 +73,6 @@ export class CircleDirection { ) { } } -let pulsetime = 1000; - export abstract class AbstractSection { public valuePath: string = ""; @@ -82,25 +80,19 @@ export abstract class AbstractSection { public circles: Circle[] = []; public square: SvgSquare; public squarePosition: SvgSquarePosition; - public pulsetimeup: number; - public pulsetimedown: number; - public pulsetimeright: number; - public pulsetimeleft: number; public name: string = ""; protected valueRatio: number = 0; protected valueText: string = ""; + protected valueText2: string = ""; protected innerRadius: number = 0; protected outerRadius: number = 0; protected height: number = 0; protected width: number = 0; + protected pulsetime = 2000; protected lastValue = { absolute: 0, ratio: 0 }; - private setPulsetime(value: number) { - pulsetime = value; - } - constructor( translateName: string, protected startAngle: number, @@ -114,7 +106,7 @@ export abstract class AbstractSection { /** * This method is called on every change of values. */ - public updateValue(absolute: number, ratio: number) { + protected updateValue(absolute: number, ratio: number) { // TODO smoothly resize the arc this.lastValue = { absolute: absolute, ratio: ratio }; this.valueRatio = this.getValueRatio(ratio); diff --git a/ui/src/app/device/overview/energymonitor/chart/section/consumptionsection.component.ts b/ui/src/app/device/overview/energymonitor/chart/section/consumptionsection.component.ts index 4ba7718d1ae..0d89cf60f83 100644 --- a/ui/src/app/device/overview/energymonitor/chart/section/consumptionsection.component.ts +++ b/ui/src/app/device/overview/energymonitor/chart/section/consumptionsection.component.ts @@ -4,8 +4,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AbstractSection, SvgSquarePosition, SvgSquare, CircleDirection, Circle } from './abstractsection.component'; -let pulsetime = 1000; -let pulsetimeright = 2000; +let PULSE = 1000; @Component({ selector: '[consumptionsection]', @@ -27,8 +26,8 @@ let pulsetimeright = 2000; fill: 'none', stroke: 'none' })), - transition('one => two', animate(pulsetime + 'ms')), - transition('two => one', animate(pulsetime + 'ms')) + transition('one => two', animate(PULSE + 'ms')), + transition('two => one', animate(PULSE + 'ms')) ]) ] }) @@ -39,19 +38,19 @@ export class ConsumptionSectionComponent extends AbstractSection implements OnIn } ngOnInit() { - Observable.interval(pulsetimeright) + Observable.interval(this.pulsetime) .subscribe(x => { if (this.lastValue.absolute > 0) { for (let i = 0; i < this.circles.length; i++) { setTimeout(() => { this.circles[i].switchState(); - }, pulsetimeright / 4 * i); + }, this.pulsetime / 4 * i); } } else if (this.lastValue.absolute < 0) { for (let i = 0; i < this.circles.length; i++) { setTimeout(() => { this.circles[this.circles.length - i - 1].switchState(); - }, pulsetimeright / 4 * i); + }, this.pulsetime / 4 * i); } } else { for (let i = 0; i < this.circles.length; i++) { @@ -60,7 +59,9 @@ export class ConsumptionSectionComponent extends AbstractSection implements OnIn } }) } - + /** + * This method is called on every change of values. + */ public updateValue(absolute: number, ratio: number) { // TODO if (absolute < 0) { @@ -87,7 +88,7 @@ export class ConsumptionSectionComponent extends AbstractSection implements OnIn protected getValueText(value: number): string { if (value == null || Number.isNaN(value)) { - return this.translate.instant('NoValue'); + return ""; } return value + " W"; diff --git a/ui/src/app/device/overview/energymonitor/chart/section/gridsection.component.ts b/ui/src/app/device/overview/energymonitor/chart/section/gridsection.component.ts index d9221f0dcc6..083c284bd52 100644 --- a/ui/src/app/device/overview/energymonitor/chart/section/gridsection.component.ts +++ b/ui/src/app/device/overview/energymonitor/chart/section/gridsection.component.ts @@ -4,8 +4,7 @@ import { Observable } from "rxjs/Rx"; import { AbstractSection, SvgSquarePosition, SvgSquare, CircleDirection, Circle } from './abstractsection.component'; -let pulsetime = 1000; -let pulsetimeleft = 2000; +let PULSE = 1000; @Component({ selector: '[gridsection]', @@ -29,8 +28,8 @@ let pulsetimeleft = 2000; })), - transition('one => two', animate(pulsetime + 'ms')), - transition('two => one', animate(pulsetime + 'ms')) + transition('one => two', animate(PULSE + 'ms')), + transition('two => one', animate(PULSE + 'ms')) ]) ] }) @@ -43,19 +42,19 @@ export class GridSectionComponent extends AbstractSection implements OnInit { } ngOnInit() { - Observable.interval(pulsetimeleft) + Observable.interval(this.pulsetime) .subscribe(x => { if (this.sellToGrid) { for (let i = 0; i < this.circles.length; i++) { setTimeout(() => { this.circles[i].switchState(); - }, pulsetimeleft / 4 * i); + }, this.pulsetime / 4 * i); } } else if (!this.sellToGrid) { for (let i = 0; i < this.circles.length; i++) { setTimeout(() => { this.circles[this.circles.length - i - 1].switchState(); - }, pulsetimeleft / 4 * i); + }, this.pulsetime / 4 * i); } } else if (this.sellToGrid == null) { for (let i = 0; i < this.circles.length; i++) { @@ -65,19 +64,16 @@ export class GridSectionComponent extends AbstractSection implements OnInit { }) } - public updateValue(absolute: number, ratio: number) { - if (absolute < 0) { - this.name = this.translate.instant('General.GridSell'); - this.sellToGrid = true; - absolute *= -1; - } else if (absolute > 0) { + public updateGridValue(buyAbsolute: number, sellAbsolute: number, ratio: number) { + if (buyAbsolute != null && buyAbsolute > 0) { this.name = this.translate.instant('General.GridBuy'); this.sellToGrid = false; + super.updateValue(buyAbsolute, ratio); } else { - this.name = this.translate.instant('General.Grid'); - this.sellToGrid = null; + this.name = this.translate.instant('General.GridSell'); + this.sellToGrid = true; + super.updateValue(sellAbsolute, ratio); } - super.updateValue(absolute, ratio); } protected getCircleDirection(): CircleDirection { @@ -109,7 +105,7 @@ export class GridSectionComponent extends AbstractSection implements OnInit { protected getValueText(value: number): string { if (value == null || Number.isNaN(value)) { - return this.translate.instant('NoValue'); + return ""; } return value + " W"; diff --git a/ui/src/app/device/overview/energymonitor/chart/section/productionsection.component.ts b/ui/src/app/device/overview/energymonitor/chart/section/productionsection.component.ts index 588c62632c4..e5058bf4f06 100644 --- a/ui/src/app/device/overview/energymonitor/chart/section/productionsection.component.ts +++ b/ui/src/app/device/overview/energymonitor/chart/section/productionsection.component.ts @@ -4,8 +4,7 @@ import { Observable } from "rxjs/Rx"; import { AbstractSection, SvgSquarePosition, SvgSquare, CircleDirection, Circle } from './abstractsection.component'; -let pulsetime = 500; -let pulsetimeup = 2000; +let PULSE = 1000; @Component({ selector: '[productionsection]', @@ -27,8 +26,8 @@ let pulsetimeup = 2000; fill: 'none', stroke: 'none' })), - transition('one => two', animate(pulsetime + 'ms')), - transition('two => one', animate(pulsetime + 'ms')) + transition('one => two', animate(PULSE + 'ms')), + transition('two => one', animate(PULSE + 'ms')) ]) ] }) @@ -39,13 +38,13 @@ export class ProductionSectionComponent extends AbstractSection implements OnIni } ngOnInit() { - Observable.interval(pulsetimeup) + Observable.interval(this.pulsetime) .subscribe(x => { if (this.lastValue.absolute > 0) { for (let i = 0; i < this.circles.length; i++) { setTimeout(() => { this.circles[this.circles.length - i - 1].switchState(); - }, pulsetime / 4 * i); + }, this.pulsetime / 4 * i); } } else if (this.lastValue.absolute == 0) { for (let i = 0; i < this.circles.length; i++) { @@ -59,6 +58,13 @@ export class ProductionSectionComponent extends AbstractSection implements OnIni }) } + /** + * This method is called on every change of values. + */ + public updateValue(absolute: number, ratio: number) { + super.updateValue(absolute, ratio) + } + protected getCircleDirection(): CircleDirection { return new CircleDirection("up"); } @@ -75,7 +81,7 @@ export class ProductionSectionComponent extends AbstractSection implements OnIni protected getValueText(value: number): string { if (value == null || Number.isNaN(value)) { - return this.translate.instant('NoValue'); + return ""; } return value + " W"; diff --git a/ui/src/app/device/overview/energymonitor/chart/section/section.component.html b/ui/src/app/device/overview/energymonitor/chart/section/section.component.html index cd06ebcb6ce..470c0d57a7c 100644 --- a/ui/src/app/device/overview/energymonitor/chart/section/section.component.html +++ b/ui/src/app/device/overview/energymonitor/chart/section/section.component.html @@ -5,6 +5,8 @@ font-family="sans-serif" font-size="square.valueRatio.fontsize" fill="color">{{name}} {{valueText}} + {{valueText2}} diff --git a/ui/src/app/device/overview/energymonitor/chart/section/storagesection.component.ts b/ui/src/app/device/overview/energymonitor/chart/section/storagesection.component.ts index a3e09db6210..d393b3a71a0 100644 --- a/ui/src/app/device/overview/energymonitor/chart/section/storagesection.component.ts +++ b/ui/src/app/device/overview/energymonitor/chart/section/storagesection.component.ts @@ -4,8 +4,7 @@ import { AbstractSection, SvgSquarePosition, SvgSquare, CircleDirection, Circle import { Observable } from "rxjs/Rx"; -let pulsetime = 1000; -let pulsetimedown = 2000; +let PULSE = 1000; @Component({ selector: '[storagesection]', @@ -27,40 +26,63 @@ let pulsetimedown = 2000; fill: 'none', stroke: 'none' })), - transition('one => two', animate(pulsetime + 'ms')), - transition('two => one', animate(pulsetime + 'ms')) + transition('one => two', animate(PULSE + 'ms')), + transition('two => one', animate(PULSE + 'ms')) ]) ] }) export class StorageSectionComponent extends AbstractSection implements OnInit { + private state: "charging" | "discharging" | "standby" = "standby"; + constructor(translate: TranslateService) { super('Device.Overview.Energymonitor.Storage', 136, 224, "#009846", translate); } ngOnInit() { - Observable.interval(pulsetimedown) + Observable.interval(this.pulsetime) .subscribe(x => { - if (this.lastValue.absolute > 0) { - for (let i = 0; i < this.circles.length; i++) { - setTimeout(() => { - this.circles[this.circles.length - i - 1].switchState(); - }, pulsetime / 4 * i); - } - } else if (this.lastValue.absolute == 0) { + if (this.state == "standby") { for (let i = 0; i < this.circles.length; i++) { this.circles[i].hide(); } - } else { + } else if (this.state == "charging") { for (let i = 0; i < this.circles.length; i++) { setTimeout(() => { this.circles[i].switchState(); - }) + }, this.pulsetime / 4 * i); + } + } else if (this.state == "discharging") { + for (let i = 0; i < this.circles.length; i++) { + setTimeout(() => { + this.circles[this.circles.length - i - 1].switchState(); + }, this.pulsetime / 4 * i); } } }) } + public updateStorageValue(chargeAbsolute: number, dischargeAbsolute: number, percentage: number) { + if (chargeAbsolute != null && chargeAbsolute > 0) { + this.name = "Speicher-Beladung" //TODO translate + super.updateValue(chargeAbsolute, percentage); + this.state = "charging"; + } else { + this.name = "Speicher-Entladung" //TODO translate + super.updateValue(dischargeAbsolute, percentage); + if (dischargeAbsolute > 0) { + this.state = "discharging"; + } else { + this.state = "standby"; + } + } + if (percentage != null) { + this.valueText2 = percentage + " %"; + } else { + this.valueText2 = ""; + } + } + protected getCircleDirection(): CircleDirection { return new CircleDirection("down"); } @@ -77,9 +99,9 @@ export class StorageSectionComponent extends AbstractSection implements OnInit { protected getValueText(value: number): string { if (value == null || Number.isNaN(value)) { - return this.translate.instant('NoValue'); + return ""; } - return this.lastValue.ratio + " %"; + return this.lastValue.absolute + " W"; } } \ No newline at end of file diff --git a/ui/src/app/device/overview/energytable/energytable.component.html b/ui/src/app/device/overview/energytable/energytable.component.html index 280ea1c7d72..c6959a5ff1f 100644 --- a/ui/src/app/device/overview/energytable/energytable.component.html +++ b/ui/src/app/device/overview/energytable/energytable.component.html @@ -19,12 +19,13 @@ - + + @@ -33,7 +34,7 @@ - + @@ -47,23 +48,48 @@ + - - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + - + + @@ -82,6 +108,7 @@
General.Soc {{ data.Soc }} %
Device.Overview.Energymonitor.ChargePower0 W
Device.Overview.Energymonitor.DischargePower {{ data.ActivePower }}var
Device.Overview.Energymonitor.ActivePowerL1{{ data.ActivePowerL1 }}WDevice.Overview.Energymonitor.ChargePowerL1{{ data.ActivePowerL1 | sign }}0W
L2{{ data.ActivePowerL2 }}{{ data.ActivePowerL2 | sign }}0W
L3{{ data.ActivePowerL3 | sign }}0W
Device.Overview.Energymonitor.DischargePowerL1{{ data.ActivePowerL1 }}0W
L2{{ data.ActivePowerL2 }}0 W
L3{{ data.ActivePowerL3 }}{{ data.ActivePowerL3 }}0 W
+ @@ -90,7 +117,7 @@ - + @@ -104,41 +131,48 @@ + - - + + - + + - - - - - - - - + + - + + - - - - + + + + + - + - - + + + + + + + + + + - - + + - + + @@ -166,7 +200,7 @@ - + diff --git a/ui/src/app/shared/chart.ts b/ui/src/app/shared/chart.ts index b7b131cb2e6..421adf7455f 100644 --- a/ui/src/app/shared/chart.ts +++ b/ui/src/app/shared/chart.ts @@ -1,9 +1,11 @@ export interface Dataset { label: string; data: number[]; + hidden: boolean; } export const EMPTY_DATASET = [{ label: "", - data: [] + data: [], + hidden: false }]; \ No newline at end of file diff --git a/ui/src/app/shared/device/config.ts b/ui/src/app/shared/device/config.ts index 0502755c36d..91f1ddb24e8 100644 --- a/ui/src/app/shared/device/config.ts +++ b/ui/src/app/shared/device/config.ts @@ -31,6 +31,8 @@ export class ConfigImpl implements DefaultTypes.Config { public readonly storageThings: string[] = []; public readonly gridMeters: string[] = []; public readonly productionMeters: string[] = []; + public readonly consumptionMeters: string[] = []; + public readonly otherMeters: string[] = []; // TODO show otherMeters in Energymonitor public readonly bridges: string[] = []; public readonly scheduler: string = null; public readonly controllers: string[] = []; @@ -42,6 +44,8 @@ export class ConfigImpl implements DefaultTypes.Config { let storageThings: string[] = [] let gridMeters: string[] = []; let productionMeters: string[] = []; + let consumptionMeters: string[] = []; + let otherMeters: string[] = []; let bridges: string[] = []; let scheduler: string = null; let controllers: string[] = []; @@ -65,8 +69,10 @@ export class ConfigImpl implements DefaultTypes.Config { gridMeters.push(thingId); } else if (thing.type === "production") { productionMeters.push(thingId); + } else if (thing.type === "consumption") { + consumptionMeters.push(thingId); } else { - console.warn("Meter without type: " + thing); + otherMeters.push(thingId); } } } diff --git a/ui/src/app/shared/device/currentdata.ts b/ui/src/app/shared/device/currentdata.ts index c10884890a3..f324ad80cfb 100644 --- a/ui/src/app/shared/device/currentdata.ts +++ b/ui/src/app/shared/device/currentdata.ts @@ -13,8 +13,10 @@ export class CurrentDataAndSummary { let result: DefaultTypes.Summary = { storage: { soc: null, - activePower: null, - maxActivePower: null + chargeActivePower: null, + maxChargeActivePower: null, + dischargeActivePower: null, + maxDischargeActivePower: null }, production: { powerRatio: null, activePower: null, // sum of activePowerAC and activePowerDC @@ -23,9 +25,10 @@ export class CurrentDataAndSummary { maxActivePower: null }, grid: { powerRatio: null, - activePower: null, - maxActivePower: null, - minActivePower: null + buyActivePower: null, + maxBuyActivePower: null, + sellActivePower: null, + maxSellActivePower: null }, consumption: { powerRatio: null, activePower: null @@ -35,6 +38,8 @@ export class CurrentDataAndSummary { { /* * Storage + * > 0 => Discharge + * < 0 => Charge */ let soc = null; let activePower = null; @@ -50,40 +55,53 @@ export class CurrentDataAndSummary { } } result.storage.soc = Utils.divideSafely(soc, countSoc); - result.storage.activePower = activePower; + if (activePower != null) { + if (activePower > 0) { + result.storage.chargeActivePower = 0; + result.storage.dischargeActivePower = activePower; + } else { + result.storage.chargeActivePower = activePower * -1; + result.storage.dischargeActivePower = 0; + } + } } { /* * Grid + * > 0 => Buy from grid + * < 0 => Sell to grid */ - let powerRatio = 0; let activePower = null; - let maxActivePower = 0; - let minActivePower = 0; + let ratio = 0; + let maxSell = 0; + let maxBuy = 0; for (let thing of config.gridMeters) { let meterData = currentData[thing]; let meterConfig = config.things[thing]; activePower = Utils.addSafely(activePower, this.getActivePower(meterData)); if ("maxActivePower" in meterConfig) { - maxActivePower += meterConfig.maxActivePower; + maxBuy += meterConfig.maxActivePower; } if ("minActivePower" in meterConfig) { - minActivePower += meterConfig.minActivePower; + maxSell += meterConfig.minActivePower; } } - // calculate ratio - if (activePower == null) { + // set GridBuy and GridSell + result.grid.maxSellActivePower = maxSell * -1; + result.grid.maxBuyActivePower = maxBuy; + if (activePower != null) { if (activePower > 0) { - powerRatio = 50 * activePower / maxActivePower + result.grid.sellActivePower = 0; + result.grid.buyActivePower = activePower; + ratio = result.grid.buyActivePower / maxSell; } else { - powerRatio = -50 * activePower / minActivePower + result.grid.sellActivePower = activePower * -1; + result.grid.buyActivePower = 0; + ratio = result.grid.sellActivePower / maxSell * -1; } } - result.grid.powerRatio = powerRatio; - result.grid.activePower = activePower; - result.grid.maxActivePower = maxActivePower; - result.grid.minActivePower = minActivePower; + result.grid.powerRatio = ratio; } { @@ -134,8 +152,14 @@ export class CurrentDataAndSummary { /* * Consumption */ - let activePower = Utils.addSafely(Utils.addSafely(result.grid.activePower, result.production.activePowerAC), result.storage.activePower); - let maxActivePower = result.grid.maxActivePower + result.production.maxActivePower + result.storage.maxActivePower; + // Consumption = GridBuy + Production + ESS-Discharge - GridSell - ESS-Charge + let minus = Utils.addSafely(result.grid.sellActivePower, result.storage.chargeActivePower); + let plus = Utils.addSafely(Utils.addSafely(result.grid.buyActivePower, result.production.activePowerAC), result.storage.dischargeActivePower); + let activePower = Utils.subtractSafely(plus, minus); + + let maxActivePower = result.grid.maxBuyActivePower - result.grid.maxSellActivePower // + + result.production.maxActivePower // + + result.storage.maxChargeActivePower - result.storage.maxDischargeActivePower; result.consumption.powerRatio = Utils.divideSafely(activePower, (maxActivePower / 100)); result.consumption.activePower = activePower; } diff --git a/ui/src/app/shared/service/defaulttypes.ts b/ui/src/app/shared/service/defaulttypes.ts index 713b4b3137c..05f7e0ebebf 100644 --- a/ui/src/app/shared/service/defaulttypes.ts +++ b/ui/src/app/shared/service/defaulttypes.ts @@ -53,8 +53,10 @@ export module DefaultTypes { export interface Summary { storage: { soc: number, - activePower: number, - maxActivePower: number + chargeActivePower: number, + maxChargeActivePower: number, + dischargeActivePower: number, + maxDischargeActivePower: number }, production: { powerRatio: number, activePower: number, // sum of activePowerAC and activePowerDC @@ -63,9 +65,10 @@ export module DefaultTypes { maxActivePower: number }, grid: { powerRatio: number, - activePower: number, - maxActivePower: number, - minActivePower: number + buyActivePower: number, + maxBuyActivePower: number, + sellActivePower: number, + maxSellActivePower: number }, consumption: { powerRatio: number, activePower: number diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index e924b68a8d0..0252dcd7e0b 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -150,6 +150,16 @@ export class Utils { } } + public static subtractSafely(v1: number, v2: number): number { + if (v1 == null) { + return v2; + } else if (v2 == null) { + return v1; + } else { + return v1 - v2; + } + } + public static divideSafely(v1: number, v2: number): number { if (v1 == null || v2 == null) { return null; diff --git a/ui/src/app/shared/translate/de.ts b/ui/src/app/shared/translate/de.ts index 96232c1345a..8f6941d2ad8 100644 --- a/ui/src/app/shared/translate/de.ts +++ b/ui/src/app/shared/translate/de.ts @@ -41,8 +41,8 @@ export const TRANSLATION = { Title: "Energiemonitor", ConsumptionWarning: "Verbrauch & unbekannte Erzeuger", Storage: "Speicher", - ChargePower: "Beladeleistung", - DischargePower: "Entladeleistung", + ChargePower: "Beladung", + DischargePower: "Entladung", ReactivePower: "Blindleistung", ActivePower: "Ausgabeleistung", GridMeter: "Netzzähler", diff --git a/ui/src/environments/openems-backend-dev-live.ts b/ui/src/environments/openems-backend-dev-live.ts new file mode 100644 index 00000000000..3811146ae5f --- /dev/null +++ b/ui/src/environments/openems-backend-dev-live.ts @@ -0,0 +1,11 @@ +import { Environment } from "../app/shared/type/environment"; +import { DefaultTypes } from '../app/shared/service/defaulttypes'; + +class OpenemsBackendDevEnvironment extends Environment { + public readonly production = false; + public readonly url = "wss://localhost:443/openems-backend-ui"; + public readonly backend: DefaultTypes.Backend = "OpenEMS Backend" + public debugMode = true; +} + +export const environment = new OpenemsBackendDevEnvironment(); diff --git a/ui/src/environments/openems-backend-dev.ts b/ui/src/environments/openems-backend-dev-local.ts similarity index 100% rename from ui/src/environments/openems-backend-dev.ts rename to ui/src/environments/openems-backend-dev-local.ts diff --git a/ui/src/environments/project-VA7282.ts b/ui/src/environments/project-VA7282.ts new file mode 100644 index 00000000000..95fff121144 --- /dev/null +++ b/ui/src/environments/project-VA7282.ts @@ -0,0 +1,114 @@ +import { Environment } from "../app/shared/type/environment"; +import { DefaultTypes } from '../app/shared/service/defaulttypes'; + +class VA7282Environment extends Environment { + public readonly production = true; + + public readonly url = "ws://" + location.hostname + ":8085"; + + public readonly backend: DefaultTypes.Backend = "OpenEMS Backend"; + + // public getCustomFields(config: Config) { + // return { + // sps0: { + // WaterLevel: { + // title: "Water level", + // unit: "centimeter" + // }, + // GetPivotOn: { + // title: "Pivot", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetBorehole1On: { + // title: "Borehole 1", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetBorehole2On: { + // title: "Borehole 2", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetBorehole3On: { + // title: "Borehole 3", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetClima1On: { + // title: "Aircondition 1", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetClima2On: { + // title: "Aircondition 2", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetOfficeOn: { + // title: "Office", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // GetTraineeCentereOn: { + // title: "Trainee Center", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // AutomaticMode: { + // title: "Automatic Mode", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // ManualMode: { + // title: "Manual Mode", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // EmergencyStop: { + // title: "Emergency Stop", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // SwitchStatePivotPump: { + // title: "Switch State Pivot Pump", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // }, + // SwitchStatePivotDrive: { + // title: "Switch State Pivot Drive", + // map: { + // 0: 'Off', + // 1: 'On' + // } + // } + // } + // } + // } +} + +export const environment = new VA7282Environment(); \ No newline at end of file diff --git a/ui/src/styles.css b/ui/src/styles.css index 237a8318d58..def1dd1e0bc 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -21,6 +21,14 @@ td.align_right { text-align: right; } +table { + border-collapse: collapse; +} + +tr.underline { + border-bottom:1pt solid lightgray; +} + .gray_color { color: gray; }
General.GridBuy0 W
General.GridSell {{ data.ActivePower | sign }}var
General.GridSell
General.GridBuy L1{{ data.ActivePowerL1 | sign }}{{ data.ActivePowerL1 }}0 W
General.GridBuyL1{{ data.ActivePowerL1 }}W
General.GridSell
L2{{ data.ActivePowerL2 | sign }}{{ data.ActivePowerL2 }}0 W
General.GridBuyL2{{ data.ActivePowerL2 }}
L3{{ data.ActivePowerL3 }}0 W
General.GridSellL3{{ data.ActivePowerL3 | sign }}L1{{ data.ActivePowerL1 | sign }}0W
L2{{ data.ActivePowerL2 | sign }}0 W
General.GridBuy
L3{{ data.ActivePowerL3 }}{{ data.ActivePowerL3 | sign }}0 W
General.Production {{ data.ActivePower }}