> data) {
+ JsonObject jTimedata = new JsonObject();
+ data.asMap().forEach((timestamp, fieldValues) -> {
+ JsonObject jTimestamp = new JsonObject();
+ fieldValues.forEach(fieldValue -> {
+ if (fieldValue instanceof NumberFieldValue) {
+ jTimestamp.addProperty(fieldValue.field, ((NumberFieldValue) fieldValue).value);
+ } else if (fieldValue instanceof StringFieldValue) {
+ jTimestamp.addProperty(fieldValue.field, ((StringFieldValue) fieldValue).value);
+ }
+ });
+ jTimedata.add(String.valueOf(timestamp), jTimestamp);
+ });
+ JsonObject j = new JsonObject();
+ j.add("timedata", jTimedata);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * config: {
+ * ...
+ * }
+ * }
+ *
+ *
+ * @param token
+ * @return
+ */
+ public static JsonObject configQueryReply(JsonObject config) {
+ JsonObject j = new JsonObject();
+ j.add("config", config);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * id: [string],
+ * currentData: {[{
+ * channel: string,
+ * value: any
+ * }]}
+ * }
+ *
+ *
+ * @return
+ */
+ public static JsonObject currentData(JsonArray jId, JsonObject jCurrentData) {
+ JsonObject j = new JsonObject();
+ j.add("id", jId);
+ j.add("currentData", jCurrentData);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * id: [string]
+ * historicData: {
+ * data: [{
+ * time: ...,
+ * channels: {
+ * thing: {
+ * channel: any
+ * }
+ * }
+ * }]
+ * }
+ * }
+ *
+ *
+ * @return
+ */
+ public static JsonObject historicDataQueryReply(JsonArray jId, JsonArray jData) {
+ JsonObject j = new JsonObject();
+ j.add("id", jId);
+ JsonObject jHistoricData = new JsonObject();
+ jHistoricData.add("data", jData);
+ j.add("historicData", jHistoricData);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * notification: {
+ * status: string,
+ * message: string,
+ * code: number,
+ * params: string[]
+ * }
+ * }
+ *
+ *
+ * @return
+ */
+ public static JsonObject notification(Notification code, Object... params) {
+ JsonObject j = new JsonObject();
+ JsonObject jNotification = new JsonObject();
+ jNotification.addProperty("status", code.getStatus());
+ jNotification.addProperty("message", String.format(code.getMessage(), params));
+ jNotification.addProperty("code", code.getValue());
+ JsonArray jParams = new JsonArray();
+ for(Object param : params) {
+ jParams.add(param.toString());
+ }
+ jNotification.add("params", jParams);
+ j.add("notification", jNotification);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * currentData: {
+ * mode: 'subscribe',
+ * channels: {}
+ * }
+ * }
+ *
+ *
+ * @return
+ */
+ public static JsonObject currentDataSubscribe(JsonArray jId, JsonObject jChannels) {
+ JsonObject j = new JsonObject();
+ j.add("id", jId);
+ JsonObject jCurrentData = new JsonObject();
+ jCurrentData.addProperty("mode", "subscribe");
+ jCurrentData.add("channels", jChannels);
+ j.add("currentData", jCurrentData);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * id: [string],
+ * log: {
+ * times: number,
+ * level: string,
+ * source: string,
+ * message: string
+ * }
+ * }
+ *
+ *
+ * @return
+ */
+ public static JsonObject log(JsonArray jId, long timestamp, String level, String source, String message) {
+ JsonObject j = new JsonObject();
+ j.add("id", jId);
+ JsonObject jLog = new JsonObject();
+ jLog.addProperty("time", timestamp);
+ jLog.addProperty("level", level);
+ jLog.addProperty("source", source);
+ jLog.addProperty("message", message);
+ j.add("log", jLog);
+ return j;
+ }
+
+ /**
+ *
+ * {
+ * id: [string],
+ * log: {
+ * mode: "unsubscribe"
+ * }
+ * }
+ *
+ *
+ * @return
+ */
+ public static JsonObject logUnsubscribe(JsonArray jId) {
+ JsonObject j = new JsonObject();
+ j.add("id", jId);
+ JsonObject jLog = new JsonObject();
+ jLog.addProperty("mode", "unsubscribe");
+ return j;
+ }
+}
diff --git a/common/src/main/java/io/openems/common/websocket/Notification.java b/common/src/main/java/io/openems/common/websocket/Notification.java
new file mode 100644
index 00000000000..6d3afa27d17
--- /dev/null
+++ b/common/src/main/java/io/openems/common/websocket/Notification.java
@@ -0,0 +1,31 @@
+package io.openems.common.websocket;
+
+public enum Notification {
+ // TODO warning, info, error should be an enum
+ EDGE_CONNECTION_ClOSED(100, "warning", "Connection [%s] was interrupted"),
+ EDGE_CONNECTION_OPENED(101, "info", "Connection [%s] was established"),
+ EDGE_UNABLE_TO_FORWARD(102, "error", "Unable to forward command to [%s]: %s"),
+ EDGE_AUTHENTICATION_BY_TOKEN_FAILED(103, "info", "Authentication by token [%s] failed");
+
+ private final int value;
+ private final String status;
+ private final String message;
+
+ private Notification(int value, String status, String message) {
+ this.value = value;
+ this.status = status;
+ this.message = message;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/common/src/main/java/io/openems/common/websocket/WebSocketUtils.java b/common/src/main/java/io/openems/common/websocket/WebSocketUtils.java
new file mode 100644
index 00000000000..3c563100d62
--- /dev/null
+++ b/common/src/main/java/io/openems/common/websocket/WebSocketUtils.java
@@ -0,0 +1,79 @@
+package io.openems.common.websocket;
+
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import org.java_websocket.WebSocket;
+import org.java_websocket.exceptions.WebsocketNotConnectedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import io.openems.common.api.TimedataSource;
+import io.openems.common.utils.JsonUtils;
+
+public class WebSocketUtils {
+
+ private static Logger log = LoggerFactory.getLogger(WebSocketUtils.class);
+
+ /**
+ * Send a message to a websocket
+ *
+ * @param j
+ * @return true if successful, otherwise false
+ */
+ public static boolean send(WebSocket websocket, JsonObject j) {
+ // System.out.println("SEND: websocket["+websocket+"]: " + j.toString());
+ try {
+ websocket.send(j.toString());
+ return true;
+ } catch (WebsocketNotConnectedException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Query history command
+ *
+ * @param j
+ */
+ public static JsonObject historicData(JsonArray jMessageId, JsonObject jHistoricData, Optional deviceId, TimedataSource timedataSource) {
+ try {
+ String mode = JsonUtils.getAsString(jHistoricData, "mode");
+ if (mode.equals("query")) {
+ /*
+ * Query historic data
+ */
+ int timezoneDiff = JsonUtils.getAsInt(jHistoricData, "timezone");
+ ZoneId timezone = ZoneId.ofOffset("", ZoneOffset.ofTotalSeconds(timezoneDiff * -1));
+ ZonedDateTime fromDate = JsonUtils.getAsZonedDateTime(jHistoricData, "fromDate", timezone);
+ ZonedDateTime toDate = JsonUtils.getAsZonedDateTime(jHistoricData, "toDate", timezone).plusDays(1);
+ JsonObject channels = JsonUtils.getAsJsonObject(jHistoricData, "channels");
+ // JsonObject kWh = JsonUtils.getAsJsonObject(jQuery, "kWh");
+ int days = Period.between(fromDate.toLocalDate(), toDate.toLocalDate()).getDays();
+ // TODO: better calculation of sensible resolution
+ int resolution = 5 * 60; // 5 Minutes
+ if (days > 25) {
+ resolution = 24 * 60 * 60; // 1 Day
+ } else if (days > 6) {
+ resolution = 3 * 60 * 60; // 3 Hours
+ } else if (days > 2) {
+ resolution = 60 * 60; // 60 Minutes
+ }
+ JsonArray jData = timedataSource.queryHistoricData(deviceId, fromDate, toDate, channels,
+ resolution);
+ // send reply
+ return DefaultMessages.historicDataQueryReply(jMessageId, jData);
+ }
+ } catch (Exception e) {
+ log.error("HistoricData Error: ", e);
+ e.printStackTrace();
+ }
+ return new JsonObject();
+ }
+}
diff --git a/backend/src/test/java/io/openems/femsserver/AppTest.java b/common/src/test/java/io/openems/common/AppTest.java
similarity index 89%
rename from backend/src/test/java/io/openems/femsserver/AppTest.java
rename to common/src/test/java/io/openems/common/AppTest.java
index 28450fa1c80..2fc153cafaa 100644
--- a/backend/src/test/java/io/openems/femsserver/AppTest.java
+++ b/common/src/test/java/io/openems/common/AppTest.java
@@ -1,4 +1,4 @@
-package io.openems.femsserver;
+package io.openems.common;
import junit.framework.Test;
import junit.framework.TestCase;
diff --git a/doc/readme.md b/doc/readme.md
index 9f021052309..99785d03085 100644
--- a/doc/readme.md
+++ b/doc/readme.md
@@ -2,208 +2,292 @@
This chapter explains the communication protocol used between the different components.
-## [1] Client (Browser) <-> Backend (OpenEMS/FemsServer)
+## [1] OpenEMS UI <-> OpenEMS Edge/OpenEMS Backend
### [1.1] Authenticate
-#### [1.1.1] At FemsServer
+[1.1.1] Authenticate Client -> OpenEMS Backend
-[1.1.1.1] Authenticate
-Cookie: session_id
+[1.1.1.1] Automatic
-[1.1.1.2] On success
-```
-{ result: { devices: ["...",] } }
-```
+Using cookie information (session_id + token) in handshake
+
+[1.1.1.1] Manual login
+
+currently forwarded to Odoo login page
+
+[1.1.2] Authenticate Client -> OpenEMS Edge
+
+[1.1.2.1] Automatic
-[1.1.1.3] On error
+// TODO
+
+[1.1.2.2] Manual login
```
-{ error }
+{
+ authenticate: {
+ mode: "login",
+ username?: string,
+ password: string
+ }
+}
```
-[1.1.1.4] Reply
+[1.1.3] Authentication reply
+
+[1.1.3.1] Authentication successful
+
```
{
authenticate: {
- mode: allow, username, token
+ mode: "allow",
+ token: string,
+ role?: "admin" | "installer" | "owner" | "guest"
}, metadata: {
- devices: [{
- name, config, online
- }],
- backend: "femsserver"
+ user: {
+ id: Integer
+ },
+ devices?: [{
+ name: string,
+ comment: string,
+ producttype: "Pro 9-12" | "MiniES 3-3" | "PRO Hybrid 9-10" | "PRO Compact 3-10" | "COMMERCIAL 40-45" | "INDUSTRIAL",
+ role: "admin" | "installer" | "owner" | "guest",
+ online: boolean
+ }]
}
}
```
-#### [1.1.2] At OpenEMS
+- authenticate.role is only sent for OpenEMS Edge
+- metadata.devices is only sent for OpenEMS Backend
-[1.1.2.1] Authenticate
-```
+[1.1.3.2] Authentication failed
{
authenticate: {
- mode: login,
- [password: "...",]
- [token: "..."]
+ mode: "deny"
}
}
+
+## [2] OpenEMS UI <-> OpenEMS Backend <-> OpenEMS Edge
+
+Following commands are all the same, no matter if UI is connected to Edge or to Backend.
+
+Backend is transparently proxying requests to a connected Edge if necessary, adding the UI token as identifier to the message id:
+
+```
+{
+ id: [..., token],
+ ...
+}
```
-[1.1.2.2] Reply
+### [2.1] Receive current configuration
+
+[2.1.1] UI -> Edge/Backend
+
```
-successful
{
- authenticate: {
- mode: allow, username, token
- }, metadata: {
- config: {},
- backend: "openems"
+ device: string,
+ id: [UUID],
+ config: {
+ mode: "query",
+ language: 'de' | 'en' | ...
}
}
-failed
+```
+
+[2.1.2] Edge/Backend -> UI
+
+```
{
- authenticate: {
- mode: deny
+ id: [UUID],
+ config: {
+ things: {
+ [id: string]: {
+ id: string,
+ class: string | string[],
+ [channel: string]: any
+ }
+ },
+ meta: {
+ [clazz: string]: {
+ implements: [string],
+ channels: {
+ [channel: string]: {
+ name: string,
+ title: string,
+ type: string | string[],
+ optional: boolean,
+ array: boolean,
+ accessLevel: string
+ }
+ }
+ }
+ }
}
}
```
-### [1.2] Current data
+### [2.2] Current live data
+
+[2.2.1] UI -> Edge/Backend
+
+For 'unsubscribe' the channels object is empty.
-[1.2.1] Subscribe
```
{
- device: "...",
- subscribe: {
+ id: [string],
+ device: string,
+ currentData: {
+ mode: "subscribe",
channels: {
- thing0: [
- channel
- ]
- },
- log: "all" | "info" | "warning" | "error"
+ [thingId: string]: string[]
+ }
}
}
```
-[1.2.2] Forward to OpenEMS
+[2.2.2] Edge/Backend -> UI
+
```
{
- subscribe: ...
+ device?: string,
+ currentData: {[{
+ channel: string,
+ value: any
+ }]}
}
```
-[1.2.3] Reply from OpenEMS
+### [2.3] Query historic data
+
+[2.3.1] UI -> Edge/Backend
```
{
- currentdata: [{
- channel, value
- }]
+ id: [string],
+ device: string,
+ historicData: {
+ mode: "query",
+ fromDate: date,
+ toDate: date,
+ timezone: number /* offset in seconds */,
+ channels: {
+ thing: [
+ channel: string
+ ]
+ }
+ // kwhChannels: {
+ // address: 'grid' | 'production' | 'storage',
+ // }
+ }
}
```
-[1.2.4] Reply
+[2.3.2] Edge/Backend -> UI
```
{
- device: "...",
- currentdata: [{
- channel, value
- }]
+ id: [UUID],
+ historicData: {
+ data: [{
+ time: string,
+ channels: {
+ thing: {
+ 'channel': 'value'
+ }
+ }
+ }]
+ // kWh: {
+ // 'meter0/ActivePower': {
+ // 'sell': ...,
+ // 'buy': ...,
+ // 'type': 'grid' | 'production' | 'storage'
+ // },
+ // 'meter1/ActivePower': {
+ // 'value': value,
+ // 'type': 'grid' | 'production' | 'storage'
+ // },
+ // 'ess0/ActivePower': {
+ // 'charge: ...,
+ // 'discharge': ...,
+ // 'type': 'grid' | 'production' | 'storage'
+ // }
+ //}
+ }
}
```
-[1.2.5] Unsubscribe
+### [2.4] Notification
+
```
{
- device: "...",
- subscribe: {
- channels: {},
- log: ""
+ notification: {
+ type: "success" | "error" | "warning" | "info",
+ message: "...",
+ code?: number,
+ params?: string[]
}
}
```
-### [1.3] Notification
+### [2.5] Current system log
+
+[2.2.1] UI -> Edge/Backend
+
```
{
- device: "...",
- notification: {
- message: "...
+ id: [string],
+ device: string,
+ log: {
+ mode: "subscribe"
}
}
```
-### [1.3] Log
```
{
- device: "...",
+ id: [string],
+ device: string,
log: {
- timestamp: ...,
- level: ...,
- source: ...,
- message: ...
+ mode: "unsubscribe"
}
}
```
-### [1.4] Query history data
+[2.2.2] Edge/Backend -> UI
-[1.4.1]
```
{
- device: "...",
- query: {
- mode: "history",
- fromDate: "01.01.2017",
- toDate: "01.01.2017",
- timezone: /* offset in seconds */,
- data: {
- channels: {
- thing: [channel]
- }
- },
- kWh: {
- "thing/channel": 'grid' | 'production' | 'storage',
- }
- }
+ log: {
+ timestamp: number,
+ level: string,
+ source: string,
+ message: string
+ }
}
```
-[1.4.2]
+## [3] OpenEMS Edge <-> OpenEMS Backend
+
+### [3.1] Timestamped data
```
{
- queryreply: {
- mode: "history",
- fromDate: "2017-01-01",
- toDate: "2017-01-01",
- timezone: /* offset in seconds */,
- data: [{
- time: ...,
- channels: {
- 'thing': {
- 'channel': 'value'
- }
- }
- }],
- kWh: {
- 'meter0/ActivePower': {
- 'sell': ...,
- 'buy': ...,
- 'type': 'grid' | 'production' | 'storage'
- },
- 'meter1/ActivePower': {
- 'value': value,
- 'type': 'grid' | 'production' | 'storage'
- },
- 'ess0/ActivePower': {
- 'charge: ...,
- 'discharge': ...,
- 'type': 'grid' | 'production' | 'storage'
- }
+ timedata: {
+ timestamp (Long): {
+ channel: String,
+ value: String | Number
}
}
}
```
+
+
+
+
+// TODO from here
+
+
### [1.5] Configuration
### [1.5.1] Update
@@ -267,29 +351,6 @@ failed
}
```
-## [2] OpenEMS <-> FemsServer
-
-### [2.1] Authenticate
-```
-{
- metadata: {
- config: {},
- backend: "openems"
- }
-}
-```
-
-### [2.2] timestamped data
-```
-{
- timedata: {
- timestamp: [{
- channel, value
- }]
- }
-}
-```
-
# Generate the docs
## Charts
diff --git a/doc/rest-api.md b/doc/rest-api.md
new file mode 100644
index 00000000000..705d70fe2ea
--- /dev/null
+++ b/doc/rest-api.md
@@ -0,0 +1,67 @@
+# OpenEMS REST-Api
+
+## Setup REST-Api-Controller
+
+OpenEMS Rest-Api is implemented as a Controller. To be active it needs to be activated in the Scheduler. Default port is 8084.
+
+Example configuration:
+```
+{
+ "class": "io.openems.impl.controller.api.rest.RestApiController",
+ "priority": 150,
+ ["port": 8084]
+}
+```
+
+
+## Channel
+
+Every "Channel" in OpenEMS can be accessed via REST-Api.
+
+To connect to the OpenEMS REST-Api you need a URL with following pattern:
+
+`http://x:@:8084/rest/channel//`
+
+* To read values you need the to use GET.
+* For write commands you have to use POST. Write commands are only possible on ConfigChannels.
+* ThingId: The id of the Controller/Device which contains the Channel you want to read/write.
+* ChannelName: the channel address to read/write.
+
+### Read Channel value
+Example: read State-of-charge (of first connected battery system/inverter named "ess0"):
+```
+http://x:@:8084/rest/channel/ess0/Soc
+```
+
+Result:
+```
+{
+ "value": 55,
+ "type": "ModbusReadLongChannel",
+ "writeable": false
+}
+```
+
+### Write Channel value
+Example: set active-power of ess0
+1. You need to activate the FixValueController:
+```
+ {
+ "id": "FixValue",
+ "class": "io.openems.impl.controller.asymmetric.fixvalue.FixValueActivePowerController",
+ "esss": [
+ "ess0"
+ ],
+ "priority": 150,
+ "activePowerL1": 0,
+ "activePowerL2": 0,
+ "activePowerL3": 0
+ }
+```
+2. Set the "activePowerL1" ConfigChannel to 1000W(Discharge)
+```
+http://x:@:8084/rest/channel/FixValue/activePowerL1
+Content-Type: application/json
+Method: POST
+Body: {"value":1000}
+```
diff --git a/edge/.classpath b/edge/.classpath
index 7cc2b7d7c69..f2f6a05c0ac 100644
--- a/edge/.classpath
+++ b/edge/.classpath
@@ -18,5 +18,6 @@
+
diff --git a/edge/resources/logback.xml b/edge/resources/logback.xml
index c08f6e5a96a..72ee2a839ed 100644
--- a/edge/resources/logback.xml
+++ b/edge/resources/logback.xml
@@ -1,34 +1,42 @@
-
+
- [%-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/openems.%d{yyyy-MM-dd}.log
+
+
+ 7
+
+
+
+ [%-8.8thread] [%-5level] [%-20.20logger{36}:%-3line]%msg%ex{10}%n
+
+
+
+
+
diff --git a/edge/src/io/openems/App.java b/edge/src/io/openems/App.java
index baa0d1f2945..a031bb45355 100644
--- a/edge/src/io/openems/App.java
+++ b/edge/src/io/openems/App.java
@@ -42,11 +42,14 @@ public static void main(String[] args) {
Config config = Config.getInstance();
config.readConfigFile();
} catch (Exception e) {
- log.error("OpenEMS start failed: " + e.getMessage());
+ log.error("OpenEMS Edge start failed: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
- log.info("OpenEMS config loaded");
+
+ log.info("OpenEMS Edge started");
+ log.info("================================================================================");
+
// kick the watchdog: READY
SDNotify.sendNotify();
}
diff --git a/edge/src/io/openems/api/bridge/Bridge.java b/edge/src/io/openems/api/bridge/Bridge.java
index 496fb31b01a..3c85f49af08 100644
--- a/edge/src/io/openems/api/bridge/Bridge.java
+++ b/edge/src/io/openems/api/bridge/Bridge.java
@@ -1,75 +1,333 @@
-/*******************************************************************************
- * 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.api.bridge;
-
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-
-import io.openems.api.device.Device;
-import io.openems.api.thing.Thing;
-import io.openems.core.utilities.AbstractWorker;
-
-public abstract class Bridge extends AbstractWorker implements Thing {
- public final static String THINGID_PREFIX = "_bridge";
- private static int instanceCounter = 0;
- protected final List devices = Collections.synchronizedList(new LinkedList());
-
- /**
- * Initialize the Thread with a name
- *
- * @param name
- */
- public Bridge() {
- super(THINGID_PREFIX + instanceCounter++);
- }
-
- @Override
- public String id() {
- return getName();
- }
-
- public synchronized void addDevice(Device device) {
- this.devices.add(device);
- }
-
- public synchronized final void addDevices(Device... devices) {
- for (Device device : devices) {
- addDevice(device);
- }
- }
-
- public synchronized final void addDevices(List devices) {
- for (Device device : devices) {
- addDevice(device);
- }
- }
-
- public synchronized void removeDevice(Device device) {
- this.devices.remove(device);
- }
-
- public synchronized List getDevices() {
- return Collections.unmodifiableList(this.devices);
- }
-
- public abstract void triggerWrite();
-}
+/*******************************************************************************
+ * 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.api.bridge;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.openems.api.channel.DebugChannel;
+import io.openems.api.device.Device;
+import io.openems.api.scheduler.Scheduler;
+import io.openems.api.thing.Thing;
+import io.openems.core.ThingRepository;
+import io.openems.core.utilities.Mutex;
+
+public abstract class Bridge extends Thread implements Thing {
+ public final static String THINGID_PREFIX = "_bridge";
+ private static int instanceCounter = 0;
+ private Scheduler scheduler;
+ private int readOtherTaskIndex = 0;
+ private int readOtherTaskCount = 0;
+ private AtomicBoolean isWriteTriggered = new AtomicBoolean(false);
+ private final AtomicBoolean initialize = new AtomicBoolean(true);
+ private final AtomicBoolean isStopped = new AtomicBoolean(false);
+ private long cycleStart = 0;
+ private Mutex initializedMutex = new Mutex(false);
+ private final AtomicBoolean isInitialized = new AtomicBoolean(false);
+ protected final List devices = Collections.synchronizedList(new LinkedList());
+ protected final Logger log;
+ private DebugChannel requiredCycleTime = new DebugChannel<>("RequiredCycleTime", this);
+
+ /**
+ * Initialize the Thread with a name
+ *
+ * @param name
+ */
+ public Bridge() {
+ log = LoggerFactory.getLogger(this.getClass());
+ setName(THINGID_PREFIX + instanceCounter++);
+ }
+
+ @Override
+ public String id() {
+ return getName();
+ }
+
+ protected List getRequiredReadTasks() {
+ List readTasks = new ArrayList<>();
+ for (Device d : devices) {
+ readTasks.addAll(d.getRequiredReadTasks());
+ }
+ return readTasks;
+ }
+
+ protected List getReadTasks() {
+ List readTasks = new ArrayList<>();
+ for (Device d : devices) {
+ readTasks.addAll(d.getReadTasks());
+ }
+ return readTasks;
+ }
+
+ protected List getWriteTasks() {
+ List writeTasks = new ArrayList<>();
+ for (Device d : devices) {
+ writeTasks.addAll(d.getWriteTasks());
+ }
+ return writeTasks;
+ }
+
+ public synchronized void addDevice(Device device) {
+ this.devices.add(device);
+ }
+
+ public synchronized final void addDevices(Device... devices) {
+ for (Device device : devices) {
+ addDevice(device);
+ }
+ }
+
+ public synchronized final void addDevices(List devices) {
+ for (Device device : devices) {
+ addDevice(device);
+ }
+ }
+
+ public synchronized void removeDevice(Device device) {
+ this.devices.remove(device);
+ }
+
+ public synchronized List getDevices() {
+ return Collections.unmodifiableList(this.devices);
+ }
+
+ public void triggerWrite() {
+ // set the Write-flag
+ isWriteTriggered.set(true);
+ }
+
+ /**
+ * This method is called when the Thread stops. Use it to close resources.
+ */
+ protected abstract void dispose();
+
+ /**
+ * This method is called once before {@link forever()} and every time after {@link restart()} method was called. Use
+ * it to (re)initialize everything.
+ *
+ * @return false on initialization error
+ */
+ protected abstract boolean initialize();
+
+ /**
+ * Little helper method: Sleep and don't let yourself interrupt by a ForceRun-Flag. It is not making sense anyway,
+ * because something is wrong with the setup if we landed here.
+ *
+ * @param duration
+ * in seconds
+ */
+ private long bridgeExceptionSleep(long duration) {
+ if (duration < 60) {
+ duration += 1;
+ }
+ long targetTime = System.nanoTime() + (duration * 1000000);
+ do {
+ try {
+ long thisDuration = (targetTime - System.nanoTime()) / 1000000;
+ if (thisDuration > 0) {
+ Thread.sleep(thisDuration);
+ }
+ } catch (InterruptedException e1) {}
+ } while (targetTime > System.nanoTime());
+ return duration;
+ }
+
+ /**
+ * Executes the Thread. Calls {@link forever} till the Thread gets interrupted.
+ */
+ @Override
+ public final void run() {
+ long bridgeExceptionSleep = 1; // seconds
+ this.initialize.set(true);
+ while (!isStopped.get()) {
+ cycleStart = System.currentTimeMillis();
+ try {
+ /*
+ * Initialize Bridge
+ */
+ while (initialize.get()) {
+ // get Scheduler
+ this.scheduler = ThingRepository.getInstance().getSchedulers().iterator().next();
+ boolean initSuccessful = initialize();
+ if (initSuccessful) {
+ isInitialized.set(true);
+ initializedMutex.release();
+ initialize.set(false);
+ } else {
+ initializedMutex.awaitOrTimeout(10000, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ this.readOtherTaskCount = 0;
+ List readTasks = this.getReadTasks();
+ List requiredReadTasks = this.getRequiredReadTasks();
+ List writeTasks = this.getWriteTasks();
+ // calculate startTime to run the read
+ long sleep = getNextReadTime() - System.currentTimeMillis() - 10;
+ if (sleep > 0) {
+ try {
+ Thread.sleep(sleep);
+ } catch (InterruptedException e) {
+ log.error("sleep failed.", e);
+ }
+ } else {
+ log.warn("cycleTime smaller than required time: " + sleep);
+ }
+ // run all tasks to read required Channels
+ for (BridgeReadTask task : requiredReadTasks) {
+ try {
+ task.runTask();
+ } catch (Exception e) {
+ log.error("failed to execute ReadTask.", e);
+ }
+ }
+ long timeUntilWrite = scheduler.getCycleStartTime() + scheduler.getRequiredTime() + 10;
+ if (readTasks.size() > 0) {
+ // calculate time until write
+ // run tasks for not required channels
+ if (timeUntilWrite - System.currentTimeMillis() > 0) {
+ readOther(readTasks, timeUntilWrite);
+ }
+ }
+ // execute write Tasks
+ boolean written = false;
+ while (!written) {
+ if (isWriteTriggered.get()) {
+ for (BridgeWriteTask task : writeTasks) {
+ try {
+ task.runTask();
+ } catch (Exception e) {
+ log.error("failed to execute WriteTask.", e);
+ }
+ }
+ isWriteTriggered.set(false);
+ written = true;
+ } else {
+ try {
+ Thread.sleep(20l);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+ // execute additional readTasks if time left
+ if (readTasks.size() > 0) {
+ if (getNextReadTime() - 10 - System.currentTimeMillis() > 0) {
+ readOther(readTasks, getNextReadTime() - 10);
+ }
+ }
+ // Everything went ok: reset bridgeExceptionSleep
+ bridgeExceptionSleep = 1;
+ } catch (Throwable e) {
+ /*
+ * Handle Bridge-Exceptions
+ */
+ log.error("Bridge-Exception! Retry later: ", e);
+ bridgeExceptionSleep = bridgeExceptionSleep(bridgeExceptionSleep);
+ }
+ requiredCycleTime.setValue(System.currentTimeMillis() - cycleStart);
+ }
+ dispose();
+ System.out.println("BridgeWorker was interrupted. Exiting gracefully...");
+ }
+
+ private void readOther(List tasks, long timeFinished) {
+ BridgeReadTask nextReadTask = null;
+ if (readOtherTaskCount + 1 < tasks.size()) {
+ nextReadTask = tasks.get(readOtherTaskIndex);
+ } else {
+ nextReadTask = null;
+ }
+ while (nextReadTask != null) {
+ if (System.currentTimeMillis() + nextReadTask.getRequiredTime() >= timeFinished) {
+ break;
+ }
+ try {
+ nextReadTask.runTask();
+ } catch (Exception e) {
+ log.error("failed to execute ReadTask.", e);
+ }
+ readOtherTaskCount++;
+ readOtherTaskIndex++;
+ readOtherTaskIndex %= tasks.size();
+ if (readOtherTaskCount + 1 < tasks.size()) {
+ nextReadTask = tasks.get(readOtherTaskIndex);
+ } else {
+ nextReadTask = null;
+ }
+ }
+ }
+
+ private long getWriteTime() {
+ long time = 0l;
+ for (BridgeWriteTask task : getWriteTasks()) {
+ time += task.getRequiredTime();
+ }
+ return time;
+ }
+
+ private long getNextReadTime() {
+ long readRequiredStartTime = scheduler.getNextCycleStart() - 10;
+ for (BridgeReadTask task : getRequiredReadTasks()) {
+ readRequiredStartTime -= task.getRequiredTime();
+ }
+ return readRequiredStartTime;
+ }
+
+ public long getRequiredCycleTime() {
+ long time = 50;
+ for (BridgeReadTask task : getRequiredReadTasks()) {
+ time += task.getRequiredTime();
+ }
+ time += scheduler.getRequiredTime();
+ for (BridgeWriteTask task : getWriteTasks()) {
+ time += task.getRequiredTime();
+ }
+ long maxReadOtherTime = 0;
+ for (BridgeReadTask task : getReadTasks()) {
+ if (task.getRequiredTime() > maxReadOtherTime) {
+ maxReadOtherTime = task.getRequiredTime();
+ }
+ }
+ time += maxReadOtherTime * 2;
+ return time;
+ }
+
+ public void shutdown() {
+ isStopped.set(true);
+ }
+
+ /**
+ * Triggers a call of {@link initialization()} in the next loop. Call this, if the configuration changes.
+ */
+ public final void triggerInitialize() {
+ initialize.set(true);
+ initializedMutex.release();
+ }
+}
diff --git a/edge/src/io/openems/api/bridge/BridgeReadTask.java b/edge/src/io/openems/api/bridge/BridgeReadTask.java
new file mode 100644
index 00000000000..2c667951cd7
--- /dev/null
+++ b/edge/src/io/openems/api/bridge/BridgeReadTask.java
@@ -0,0 +1,5 @@
+package io.openems.api.bridge;
+
+public abstract class BridgeReadTask extends BridgeTask {
+
+}
diff --git a/edge/src/io/openems/api/bridge/BridgeTask.java b/edge/src/io/openems/api/bridge/BridgeTask.java
new file mode 100644
index 00000000000..2a27deafaaa
--- /dev/null
+++ b/edge/src/io/openems/api/bridge/BridgeTask.java
@@ -0,0 +1,39 @@
+package io.openems.api.bridge;
+
+import java.util.Queue;
+
+import com.google.common.collect.EvictingQueue;
+import com.google.common.collect.Queues;
+
+public abstract class BridgeTask {
+
+ private Queue requiredTimes;
+
+ public BridgeTask() {
+ requiredTimes = EvictingQueue.create(5);
+ for (int i = 0; i < 5; i++) {
+ requiredTimes.add(0L);
+ }
+ }
+
+ public long getRequiredTime() {
+ synchronized (requiredTimes) {
+ long sum = 0;
+ for (Long l : requiredTimes) {
+ sum += l;
+ }
+ return sum / requiredTimes.size();
+ }
+ }
+
+ public void runTask() throws Exception {
+ long timeBefore = System.currentTimeMillis();
+ this.run();
+ synchronized (requiredTimes) {
+ this.requiredTimes.add(System.currentTimeMillis() - timeBefore);
+ }
+ }
+
+ protected abstract void run() throws Exception;
+
+}
diff --git a/edge/src/io/openems/api/bridge/BridgeWriteTask.java b/edge/src/io/openems/api/bridge/BridgeWriteTask.java
new file mode 100644
index 00000000000..3c5360dea69
--- /dev/null
+++ b/edge/src/io/openems/api/bridge/BridgeWriteTask.java
@@ -0,0 +1,5 @@
+package io.openems.api.bridge;
+
+public abstract class BridgeWriteTask extends BridgeTask {
+
+}
diff --git a/edge/src/io/openems/api/channel/ConfigChannel.java b/edge/src/io/openems/api/channel/ConfigChannel.java
index 31b41875618..329645069c1 100644
--- a/edge/src/io/openems/api/channel/ConfigChannel.java
+++ b/edge/src/io/openems/api/channel/ConfigChannel.java
@@ -125,4 +125,9 @@ public ConfigChannel label(T value, String label) {
super.label(value, label);
return this;
}
+
+ @Override
+ public ConfigChannel doNotPersist() {
+ return (ConfigChannel) super.doNotPersist();
+ }
}
diff --git a/edge/src/io/openems/api/channel/DebugChannel.java b/edge/src/io/openems/api/channel/DebugChannel.java
new file mode 100644
index 00000000000..e58beb2f757
--- /dev/null
+++ b/edge/src/io/openems/api/channel/DebugChannel.java
@@ -0,0 +1,43 @@
+package io.openems.api.channel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openems.api.thing.Thing;
+import io.openems.core.Databus;
+
+public class DebugChannel extends ReadChannel {
+ private static List> debugChannels = new ArrayList<>();
+ private static boolean DEBUGENABLED = false;
+ private static Databus bus = Databus.getInstance();
+
+ public static void enableDebug() {
+ DEBUGENABLED = true;
+ for (DebugChannel> channel : debugChannels) {
+ channel.addChangeListener(bus);
+ channel.addUpdateListener(bus);
+ }
+ }
+
+ public static void disableDebug() {
+ DEBUGENABLED = false;
+ for (DebugChannel> channel : debugChannels) {
+ channel.removeChangeListener(bus);
+ channel.removeUpdateListener(bus);
+ }
+ }
+
+ public DebugChannel(String id, Thing parent) {
+ super(id, parent);
+ if (DEBUGENABLED) {
+ this.addChangeListener(bus);
+ this.addUpdateListener(bus);
+ }
+ debugChannels.add(this);
+ }
+
+ public void setValue(T value) {
+ this.updateValue(value);
+ }
+
+}
diff --git a/edge/src/io/openems/api/channel/ReadChannel.java b/edge/src/io/openems/api/channel/ReadChannel.java
index 4e88f6264a2..910bd4dc269 100644
--- a/edge/src/io/openems/api/channel/ReadChannel.java
+++ b/edge/src/io/openems/api/channel/ReadChannel.java
@@ -55,6 +55,7 @@ public class ReadChannel implements Channel, Comparable> {
private String unit = "";
private boolean isRequired = false;
protected final Set users = new HashSet<>();
+ protected boolean doNotPersist = false;
private final Set updateListeners = ConcurrentHashMap.newKeySet();
private final Set changeListeners = ConcurrentHashMap.newKeySet();
@@ -132,6 +133,11 @@ public ReadChannel user(User... users) {
return this;
}
+ public ReadChannel doNotPersist() {
+ this.doNotPersist = true;
+ return this;
+ }
+
/*
* Getter
*/
@@ -303,6 +309,10 @@ public boolean isRequired() {
return this.isRequired;
}
+ public boolean isDoNotPersist() {
+ return doNotPersist;
+ }
+
@Override
public JsonObject toJsonObject() throws NotImplementedException {
JsonObject j = new JsonObject();
diff --git a/edge/src/io/openems/api/device/Device.java b/edge/src/io/openems/api/device/Device.java
index 7d7061b16c5..5202cf3b045 100644
--- a/edge/src/io/openems/api/device/Device.java
+++ b/edge/src/io/openems/api/device/Device.java
@@ -1,55 +1,101 @@
-/*******************************************************************************
- * 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.api.device;
-
-import java.util.Set;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import io.openems.api.bridge.Bridge;
-import io.openems.api.device.nature.DeviceNature;
-import io.openems.api.exception.OpenemsException;
-import io.openems.api.thing.Thing;
-
-public abstract class Device implements Thing {
- public final static String THINGID_PREFIX = "_device";
- private static int instanceCounter = 0;
- protected final Logger log;
- private Bridge bridge = null;
- private final String thingId;
-
- public Device() throws OpenemsException {
- this.thingId = THINGID_PREFIX + instanceCounter++;
- log = LoggerFactory.getLogger(this.getClass());
- }
-
- public Bridge getBridge() {
- return bridge;
- }
-
- @Override
- public String id() {
- return this.thingId;
- }
-
- protected abstract Set getDeviceNatures();
-}
+/*******************************************************************************
+ * 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.api.device;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.openems.api.bridge.Bridge;
+import io.openems.api.bridge.BridgeReadTask;
+import io.openems.api.bridge.BridgeWriteTask;
+import io.openems.api.device.nature.DeviceNature;
+import io.openems.api.exception.OpenemsException;
+import io.openems.api.thing.Thing;
+
+public abstract class Device implements Thing {
+ public final static String THINGID_PREFIX = "_device";
+ private static int instanceCounter = 0;
+ protected final Logger log;
+ private Bridge bridge = null;
+ private final String thingId;
+
+ public Device(Bridge parent) throws OpenemsException {
+ this.thingId = THINGID_PREFIX + instanceCounter++;
+ log = LoggerFactory.getLogger(this.getClass());
+ this.bridge = parent;
+ }
+
+ public Bridge getBridge() {
+ return bridge;
+ }
+
+ @Override
+ public String id() {
+ return this.thingId;
+ }
+
+ @Override
+ public void init() {
+ Thing.super.init();
+ for (DeviceNature nature : getDeviceNatures()) {
+ nature.init();
+ }
+ }
+
+ protected abstract Set getDeviceNatures();
+
+ public List getRequiredReadTasks() {
+ List readTasks = new ArrayList<>();
+ for (DeviceNature nature : getDeviceNatures()) {
+ List natureRequiredReadTasks = nature.getRequiredReadTasks();
+ if (natureRequiredReadTasks != null) {
+ readTasks.addAll(natureRequiredReadTasks);
+ }
+ }
+ return readTasks;
+ }
+
+ public List getReadTasks() {
+ List readTasks = new ArrayList<>();
+ for (DeviceNature nature : getDeviceNatures()) {
+ List natureReadTasks = nature.getReadTasks();
+ if (natureReadTasks != null) {
+ readTasks.addAll(natureReadTasks);
+ }
+ }
+ return readTasks;
+ }
+
+ public List getWriteTasks() {
+ List writeTasks = new ArrayList<>();
+ for (DeviceNature nature : getDeviceNatures()) {
+ List natureWriteTasks = nature.getWriteTasks();
+ if (natureWriteTasks != null) {
+ writeTasks.addAll(natureWriteTasks);
+ }
+ }
+ return writeTasks;
+ }
+}
diff --git a/edge/src/io/openems/api/device/nature/DeviceNature.java b/edge/src/io/openems/api/device/nature/DeviceNature.java
index f9f1250ca11..c2f90211a8b 100644
--- a/edge/src/io/openems/api/device/nature/DeviceNature.java
+++ b/edge/src/io/openems/api/device/nature/DeviceNature.java
@@ -1,28 +1,42 @@
-/*******************************************************************************
- * 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.api.device.nature;
-
-import io.openems.api.channel.Channel;
-import io.openems.api.thing.Thing;
-
-public interface DeviceNature extends Thing {
- public void setAsRequired(Channel channel);
-}
+/*******************************************************************************
+ * 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.api.device.nature;
+
+import java.util.List;
+
+import io.openems.api.bridge.BridgeReadTask;
+import io.openems.api.bridge.BridgeWriteTask;
+import io.openems.api.channel.Channel;
+import io.openems.api.device.Device;
+import io.openems.api.thing.Thing;
+
+public interface DeviceNature extends Thing {
+
+ public Device getParent();
+
+ public void setAsRequired(Channel channel);
+
+ public List getRequiredReadTasks();
+
+ public List getReadTasks();
+
+ public List getWriteTasks();
+}
diff --git a/edge/src/io/openems/api/doc/ConfigChannelDoc.java b/edge/src/io/openems/api/doc/ConfigChannelDoc.java
index b140ce76956..4550c67cb47 100644
--- a/edge/src/io/openems/api/doc/ConfigChannelDoc.java
+++ b/edge/src/io/openems/api/doc/ConfigChannelDoc.java
@@ -22,7 +22,10 @@
import com.google.gson.JsonObject;
+import io.openems.api.controller.IsThingMap;
+import io.openems.api.controller.ThingMap;
import io.openems.api.security.User;
+import io.openems.core.utilities.InjectionUtils;
public class ConfigChannelDoc {
private final String name;
@@ -50,7 +53,14 @@ public JsonObject getAsJsonObject() {
JsonObject j = new JsonObject();
j.addProperty("name", name);
j.addProperty("title", title);
- j.addProperty("type", type.getSimpleName());
+ if (ThingMap.class.isAssignableFrom(type)) {
+ // for ThingMap type: get the types from annotation and return JsonArray
+ IsThingMap isThingMapAnnotation = type.getAnnotation(IsThingMap.class);
+ j.add("type", InjectionUtils.getImplementsAsJson(isThingMapAnnotation.type()));
+ } else {
+ // for simple types, use only simple name (e.g. 'Long', 'Integer',...)
+ j.addProperty("type", type.getSimpleName());
+ }
j.addProperty("optional", optional);
j.addProperty("array", array);
j.addProperty("accessLevel", accessLevel.name().toLowerCase());
diff --git a/edge/src/io/openems/api/doc/ThingDoc.java b/edge/src/io/openems/api/doc/ThingDoc.java
index 5213c86de19..0e311df9a38 100644
--- a/edge/src/io/openems/api/doc/ThingDoc.java
+++ b/edge/src/io/openems/api/doc/ThingDoc.java
@@ -26,6 +26,7 @@
import com.google.gson.JsonObject;
import io.openems.api.thing.Thing;
+import io.openems.core.utilities.InjectionUtils;
public class ThingDoc {
@@ -69,6 +70,7 @@ public JsonObject getAsJsonObject() {
jChannels.add(config.getName(), config.getAsJsonObject());
}
j.add("channels", jChannels);
+ j.add("implements", InjectionUtils.getImplementsAsJson(getClazz()));
return j;
}
}
diff --git a/edge/src/io/openems/api/exception/OpenemsException.java b/edge/src/io/openems/api/exception/OpenemsException.java
index c1e2f779cc8..bcd3eb435f5 100644
--- a/edge/src/io/openems/api/exception/OpenemsException.java
+++ b/edge/src/io/openems/api/exception/OpenemsException.java
@@ -20,7 +20,7 @@
*******************************************************************************/
package io.openems.api.exception;
-public class OpenemsException extends Exception {
+public class OpenemsException extends io.openems.common.exceptions.OpenemsException {
private static final long serialVersionUID = 2244921342821407476L;
diff --git a/edge/src/io/openems/api/persistence/QueryablePersistence.java b/edge/src/io/openems/api/persistence/QueryablePersistence.java
index 4eff80ff0d3..a40fc44ef33 100644
--- a/edge/src/io/openems/api/persistence/QueryablePersistence.java
+++ b/edge/src/io/openems/api/persistence/QueryablePersistence.java
@@ -20,38 +20,8 @@
*******************************************************************************/
package io.openems.api.persistence;
-import java.time.ZonedDateTime;
+import io.openems.common.api.TimedataSource;
-import com.google.gson.JsonObject;
+public abstract class QueryablePersistence extends Persistence implements TimedataSource {
-import io.openems.api.exception.OpenemsException;
-
-public abstract class QueryablePersistence extends Persistence {
-
- /**
- *
- *
- * @param fromDate
- * @param toDate
- * @param channels
- * @param resolution
- * in seconds
- * @return
- * @throws OpenemsException
- *
- *
- * Returns:
- * [{
- * timestamp: "2017-03-21T08:55:20Z",
- * channels: {
- * 'thing': {
- * 'channel': 'value'
- * }
- * }
- * }]
- }
- *
- */
- public abstract JsonObject query(ZonedDateTime fromDate, ZonedDateTime toDate, JsonObject channels, int resolution)
- throws OpenemsException;
}
diff --git a/edge/src/io/openems/api/scheduler/Scheduler.java b/edge/src/io/openems/api/scheduler/Scheduler.java
index 1dfeac574cc..dd0ce50841e 100644
--- a/edge/src/io/openems/api/scheduler/Scheduler.java
+++ b/edge/src/io/openems/api/scheduler/Scheduler.java
@@ -1,55 +1,114 @@
-/*******************************************************************************
- * 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.api.scheduler;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import io.openems.api.controller.Controller;
-import io.openems.api.exception.ConfigException;
-import io.openems.api.exception.ReflectionException;
-import io.openems.api.thing.Thing;
-import io.openems.core.utilities.AbstractWorker;
-
-public abstract class Scheduler extends AbstractWorker implements Thing {
- public final static String THINGID_PREFIX = "_scheduler";
- private static int instanceCounter = 0;
- protected final Map controllers = new ConcurrentHashMap<>();
-
- public Scheduler() {
- super(THINGID_PREFIX + instanceCounter++);
- }
-
- public synchronized void addController(Controller controller) throws ReflectionException, ConfigException {
- controllers.put(controller.id(), controller);
- }
-
- public synchronized void removeController(Controller controller) {
- controllers.remove(controller.id());
- }
-
- public synchronized List getControllers() {
- return Collections.unmodifiableList(new ArrayList<>(this.controllers.values()));
- }
-}
+/*******************************************************************************
+ * 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.api.scheduler;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.openems.api.bridge.Bridge;
+import io.openems.api.channel.ConfigChannel;
+import io.openems.api.controller.Controller;
+import io.openems.api.doc.ConfigInfo;
+import io.openems.api.exception.ConfigException;
+import io.openems.api.exception.ReflectionException;
+import io.openems.api.thing.Thing;
+import io.openems.core.ThingRepository;
+import io.openems.core.utilities.AbstractWorker;
+
+public abstract class Scheduler extends AbstractWorker implements Thing {
+ private final static int DEFAULT_CYCLETIME = 1000;
+ public final static String THINGID_PREFIX = "_scheduler";
+ private static int instanceCounter = 0;
+ protected final Map controllers = new ConcurrentHashMap<>();
+ private long requiredTime = 0;
+ private long cycleStartTime = 0;
+ protected final ThingRepository thingRepository;
+ private Integer actualCycleTime = null;
+
+ /*
+ * Config
+ */
+ @ConfigInfo(title = "Sets the duration of each cycle in milliseconds", type = Integer.class, isOptional = true)
+ public ConfigChannel cycleTime = new ConfigChannel("cycleTime", this)
+ .defaultValue(DEFAULT_CYCLETIME);
+
+ @Override
+ protected int getCycleTime() {
+ int time = cycleTime.valueOptional().orElse(DEFAULT_CYCLETIME);
+ if (actualCycleTime != null) {
+ time = actualCycleTime;
+ }
+ return time;
+ }
+
+ public Scheduler() {
+ super(THINGID_PREFIX + instanceCounter++);
+ thingRepository = ThingRepository.getInstance();
+ }
+
+ public synchronized void addController(Controller controller) throws ReflectionException, ConfigException {
+ controllers.put(controller.id(), controller);
+ }
+
+ public synchronized void removeController(Controller controller) {
+ controllers.remove(controller.id());
+ }
+
+ public synchronized List getControllers() {
+ return Collections.unmodifiableList(new ArrayList<>(this.controllers.values()));
+ }
+
+ @Override
+ protected void forever() {
+ cycleStartTime = System.currentTimeMillis();
+ execute();
+ requiredTime = System.currentTimeMillis() - cycleStartTime;
+ long maxTime = 0;
+ for (Bridge bridge : thingRepository.getBridges()) {
+ if (bridge.getRequiredCycleTime() > maxTime) {
+ maxTime = bridge.getRequiredCycleTime();
+ }
+ }
+ maxTime = (maxTime + 100) / 100 * 100;
+ if (maxTime > cycleTime.valueOptional().orElse(500)) {
+ actualCycleTime = (int) maxTime;
+ } else {
+ actualCycleTime = cycleTime.valueOptional().orElse(500);
+ }
+ }
+
+ protected abstract void execute();
+
+ public long getRequiredTime() {
+ return requiredTime;
+ }
+
+ public long getNextCycleStart() {
+ return cycleStartTime + getCycleTime();
+ }
+
+ public long getCycleStartTime() {
+ return cycleStartTime;
+ }
+}
diff --git a/edge/src/io/openems/api/security/Authentication.java b/edge/src/io/openems/api/security/Authentication.java
deleted file mode 100644
index 6fcc7334866..00000000000
--- a/edge/src/io/openems/api/security/Authentication.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*******************************************************************************
- * 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.api.security;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class Authentication {
- private final static Logger log = LoggerFactory.getLogger(Authentication.class);
-
- /*
- * Singleton
- */
- private static Authentication instance;
-
- public static synchronized Authentication getInstance() {
- if (Authentication.instance == null) {
- Authentication.instance = new Authentication();
- }
- return Authentication.instance;
- }
-
- /*
- * This class
- */
- private final ConcurrentHashMap sessions = new ConcurrentHashMap<>();
-
- private Authentication() {}
-
- public Session byUserPassword(String username, String password) {
- User user = User.authenticate(username, password);
- Session session = addSession(user);
- if (session != null) {
- log.info("User[" + session.getUser().getName() + "] authenticated. " + //
- "Created session[" + session.getToken() + "].");
- } else {
- log.info("Authentication by username[" + username + "] + password failed.");
- }
- return session;
- }
-
- public Session byPassword(String password) {
- User user = User.authenticate(password);
- Session session = addSession(user);
- if (session != null) {
- log.info("User[" + session.getUser().getName() + "] authenticated. " + //
- "Created session[" + session.getToken() + "].");
- } else {
- log.info("Authentication by password failed.");
- }
- return session;
- }
-
- public Session bySession(String token) {
- Session session = sessions.get(token);
- if (session != null) {
- log.info("User[" + session.getUser().getName() + "] authenticated by " + //
- "session[" + session.getToken() + "].");
- } else {
- log.info("Authentication by session failed.");
- }
- return session;
- }
-
- private Session addSession(User user) {
- if (user != null) {
- Session session = new Session(user);
- sessions.put(session.getToken(), session);
- return session;
- }
- return null;
- }
-}
diff --git a/edge/src/io/openems/api/security/User.java b/edge/src/io/openems/api/security/User.java
index fc706cf29c6..490150ec17b 100644
--- a/edge/src/io/openems/api/security/User.java
+++ b/edge/src/io/openems/api/security/User.java
@@ -27,6 +27,7 @@
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Decoder;
+import java.util.Optional;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
@@ -36,8 +37,8 @@
import org.slf4j.LoggerFactory;
import io.openems.api.exception.OpenemsException;
+import io.openems.common.utils.SecureRandomSingleton;
import io.openems.core.Config;
-import io.openems.core.utilities.SecureRandomSingleton;
public enum User {
/*
@@ -129,24 +130,28 @@ private static byte[] getRandomSalt(int length) {
* @param password
* @return the authenticated User or null if authentication failed
*/
- public static User authenticate(String password) {
+ public static Optional authenticate(String password) {
// Search for any user with the given password
for (User user : USERS) {
if (user.checkPassword(password)) {
- return user;
+ log.info("Authentication successful with password only for user [" + user.getName() + "].");
+ return Optional.ofNullable(user);
}
}
- return null;
+ log.info("Authentication failed with password only.");
+ return Optional.empty();
}
- public static User authenticate(String username, String password) {
+ public static Optional authenticate(String username, String password) {
// Search for user with given username
for (User user : USERS) {
if (username.equals(user.getName())) {
if (user.checkPassword(password)) {
- return user;
+ log.info("Authentication successful for user[" + username + "].");
+ return Optional.of(user);
} else {
- return null;
+ log.info("Authentication failed for user[" + username + "]: wrong password");
+ return Optional.empty();
}
}
}
diff --git a/edge/src/io/openems/core/Address.java b/edge/src/io/openems/core/Address.java
index bc19f951750..9a7f3792261 100644
--- a/edge/src/io/openems/core/Address.java
+++ b/edge/src/io/openems/core/Address.java
@@ -1,5 +1,6 @@
package io.openems.core;
+// TODO move this to common package (types/ChannelAddress)
public class Address {
private final String thingId;
private final String channelId;
diff --git a/edge/src/io/openems/core/ClassRepository.java b/edge/src/io/openems/core/ClassRepository.java
index 567ca979043..f473cfe7aaa 100644
--- a/edge/src/io/openems/core/ClassRepository.java
+++ b/edge/src/io/openems/core/ClassRepository.java
@@ -34,6 +34,7 @@
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import io.openems.api.bridge.Bridge;
@@ -99,6 +100,14 @@ public Map getThingConfigChannels(Class extends Thing> cla
return Collections.unmodifiableMap(thingConfigChannels.row(clazz));
}
+ public Iterable getAvailableThings() throws ReflectionException {
+ return Iterables.concat( //
+ getAvailableBridges(), //
+ getAvailableControllers(), //
+ getAvailableDevices(), //
+ getAvailableSchedulers());
+ }
+
public Collection getAvailableControllers() throws ReflectionException {
if (controllers.isEmpty()) {
for (Class extends Thing> clazz : ConfigUtils.getAvailableClasses("io.openems.impl.controller",
@@ -144,41 +153,39 @@ public Collection getAvailableSchedulers() throws ReflectionException
}
private void parseClass(Class extends Thing> clazz) {
- if (Thing.class.isAssignableFrom(Thing.class)) {
- for (Method method : clazz.getMethods()) {
- Class> type = null;
- if (method.getReturnType().isArray()) {
- Class> rtype = method.getReturnType();
- type = rtype.getComponentType();
+ for (Method method : clazz.getMethods()) {
+ Class> type = null;
+ if (method.getReturnType().isArray()) {
+ Class> rtype = method.getReturnType();
+ type = rtype.getComponentType();
+ } else {
+ type = method.getReturnType();
+ }
+ if (Channel.class.isAssignableFrom(type)) {
+ thingChannels.put(clazz, method);
+ }
+ if (ConfigChannel.class.isAssignableFrom(type)) {
+ ConfigInfo configAnnotation = getAnnotation(clazz, method.getName());
+ if (configAnnotation != null) {
+ thingConfigChannels.put(clazz, method, configAnnotation);
} else {
- type = method.getReturnType();
- }
- if (Channel.class.isAssignableFrom(type)) {
- thingChannels.put(clazz, method);
- }
- if (ConfigChannel.class.isAssignableFrom(type)) {
- ConfigInfo configAnnotation = getAnnotation(clazz, method.getName());
- if (configAnnotation != null) {
- thingConfigChannels.put(clazz, method, configAnnotation);
- } else {
- log.error("Config-Annotation is missing for method [" + method.getName() + "] in class ["
- + clazz.getName() + "]");
- }
+ log.error("Config-Annotation is missing for method [" + method.getName() + "] in class ["
+ + clazz.getName() + "]");
}
}
- for (Field field : clazz.getFields()) {
- Class> type = field.getType();
- if (Channel.class.isAssignableFrom(type)) {
- thingChannels.put(clazz, field);
- }
- if (ConfigChannel.class.isAssignableFrom(type)) {
- ConfigInfo configAnnotation = field.getAnnotation(ConfigInfo.class);
- if (configAnnotation == null) {
- log.error("Config-Annotation is missing for field [" + field.getName() + "] in class ["
- + clazz.getName() + "]");
- } else {
- thingConfigChannels.put(clazz, field, configAnnotation);
- }
+ }
+ for (Field field : clazz.getFields()) {
+ Class> type = field.getType();
+ if (Channel.class.isAssignableFrom(type)) {
+ thingChannels.put(clazz, field);
+ }
+ if (ConfigChannel.class.isAssignableFrom(type)) {
+ ConfigInfo configAnnotation = field.getAnnotation(ConfigInfo.class);
+ if (configAnnotation == null) {
+ log.error("Config-Annotation is missing for field [" + field.getName() + "] in class ["
+ + clazz.getName() + "]");
+ } else {
+ thingConfigChannels.put(clazz, field, configAnnotation);
}
}
}
diff --git a/edge/src/io/openems/core/Config.java b/edge/src/io/openems/core/Config.java
index d1f7c959273..642d6e09549 100644
--- a/edge/src/io/openems/core/Config.java
+++ b/edge/src/io/openems/core/Config.java
@@ -1,556 +1,541 @@
-/*******************************************************************************
- * 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.core;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-import io.openems.api.bridge.Bridge;
-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.Device;
-import io.openems.api.doc.ThingDoc;
-import io.openems.api.exception.ConfigException;
-import io.openems.api.exception.NotImplementedException;
-import io.openems.api.exception.OpenemsException;
-import io.openems.api.exception.ReflectionException;
-import io.openems.api.exception.WriteChannelException;
-import io.openems.api.persistence.Persistence;
-import io.openems.api.scheduler.Scheduler;
-import io.openems.api.security.User;
-import io.openems.core.utilities.AbstractWorker;
-import io.openems.core.utilities.ConfigUtils;
-import io.openems.core.utilities.InjectionUtils;
-import io.openems.core.utilities.JsonUtils;
-
-public class Config implements ChannelChangeListener {
-
- private final static Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
- private final static String CONFIG_PATH = "etc/openems.d";
- private final static String CONFIG_FILE_NAME = "config.json";
- private final static String CONFIG_BACKUP_FILE_NAME = "config.backup.json";
-
- private final static Logger log = LoggerFactory.getLogger(Config.class);
- private static Config instance;
-
- public static synchronized Config getInstance() throws ConfigException {
- if (Config.instance == null) {
- Config.instance = new Config();
- }
- return Config.instance;
- }
-
- private final ThingRepository thingRepository;
- private final Path configFile;
- private final Path configBackupFile;
- private final ExecutorService writeConfigExecutor;
-
- public Config() throws ConfigException {
- thingRepository = ThingRepository.getInstance();
- this.configFile = getConfigFile();
- this.configBackupFile = getConfigBackupFile();
- this.writeConfigExecutor = Executors.newSingleThreadExecutor();
- }
-
- public synchronized void readConfigFile() throws Exception {
- // Read configuration from default config file
- try {
- readConfigFromFile(configFile);
- log.info("Read configuration from file [" + configFile.toString() + "]");
- return;
- } catch (Exception e) {
- log.warn("Failed to read configuration from file [" + configFile.toString() + "] ", e);
- }
- // Read configuration from backup config file
- try {
- readConfigFromFile(configBackupFile);
- log.info("Read configuration from backup file [" + configBackupFile.toString() + "]");
- } catch (Exception e) {
- log.warn("Failed to read configuration backup file [" + configFile.toString() + "]", e);
- throw e;
- }
- }
-
- private synchronized void readConfigFromFile(Path path) throws Exception {
- JsonObject jConfig = new JsonObject();
- JsonParser parser = new JsonParser();
- String config = new String(Files.readAllBytes(path), DEFAULT_CHARSET);
- JsonElement jsonElement = parser.parse(config);
- jConfig = jsonElement.getAsJsonObject();
- jConfig = addDefaultConfig(jConfig);
- // apply config
- parseJsonConfig(jConfig);
- }
-
- private JsonObject addDefaultConfig(JsonObject jConfig) {
- try {
- /*
- * Things
- */
- JsonArray jThings;
- if (!jConfig.has("things") || !jConfig.get("things").isJsonArray()) {
- jThings = new JsonArray();
- } else {
- jThings = JsonUtils.getAsJsonArray(jConfig, "things");
- }
- {
- /*
- * Add SystemBridge
- */
- if (!JsonUtils.hasElement(jConfig, "things", "class", "io.openems.impl.protocol.system.SystemBridge")) {
- JsonObject jBridge = new JsonObject();
- {
- jBridge.addProperty("class", "io.openems.impl.protocol.system.SystemBridge");
- JsonArray jDevices = new JsonArray();
- {
- JsonObject jSystem = new JsonObject();
- jSystem.addProperty("class", "io.openems.impl.device.system.System");
- {
- JsonObject jSystemNature = new JsonObject();
- {
- jSystemNature.addProperty("id", "system0");
- }
- jSystem.add("system", jSystemNature);
- }
- jDevices.add(jSystem);
- }
- jBridge.add("devices", jDevices);
- }
- jThings.add(jBridge);
- }
- }
- jConfig.add("things", jThings);
- /*
- * Scheduler
- */
- JsonObject jScheduler;
- if (!jConfig.has("scheduler") || !jConfig.get("scheduler").isJsonObject()) {
- jScheduler = new JsonObject();
- jScheduler.addProperty("class", "io.openems.impl.scheduler.SimpleScheduler");
- } else {
- jScheduler = JsonUtils.getAsJsonObject(jConfig, "scheduler");
- }
- {
- /*
- * Controller
- */
- JsonArray jControllers;
- if (!jScheduler.has("controllers") || !jScheduler.get("controllers").isJsonArray()) {
- jControllers = new JsonArray();
- } else {
- jControllers = JsonUtils.getAsJsonArray(jScheduler, "controllers");
- }
- {
- /*
- * WebsocketApiController
- */
- if (!JsonUtils.hasElement(jControllers, "class",
- "io.openems.impl.controller.api.websocket.WebsocketApiController")) {
- JsonObject jWebsocketApiController = new JsonObject();
- jWebsocketApiController.addProperty("class",
- "io.openems.impl.controller.api.websocket.WebsocketApiController");
- jWebsocketApiController.addProperty("priority", Integer.MIN_VALUE);
- jControllers.add(jWebsocketApiController);
- }
- /*
- * RestApiController
- */
- if (!JsonUtils.hasElement(jControllers, "class",
- "io.openems.impl.controller.api.rest.RestApiController")) {
- JsonObject jRestApiController = new JsonObject();
- jRestApiController.addProperty("class",
- "io.openems.impl.controller.api.rest.RestApiController");
- jRestApiController.addProperty("priority", Integer.MIN_VALUE);
- jControllers.add(jRestApiController);
- }
- }
- jScheduler.add("controllers", jControllers);
- }
- jConfig.add("scheduler", jScheduler);
- } catch (ReflectionException e) {
- log.warn("Error applying default config: " + e.getMessage());
- }
- return jConfig;
- }
-
- /**
- * Writes the config file. Holds a backup config file and restores it on error. Method is executed asynchronously.
- *
- * @throws NotImplementedException
- */
- public void writeConfigFile() throws NotImplementedException {
- JsonObject jConfig = getJsonComplete();
- Runnable writeConfigRunnable = new Runnable() {
- @Override
- public void run() {
- Gson gson = new GsonBuilder().setPrettyPrinting().create();
- String config = gson.toJson(jConfig);
- try {
- /*
- * create backup of config file
- */
- Files.copy(configFile, configBackupFile, StandardCopyOption.REPLACE_EXISTING);
- } catch (IOException e) {
- ConfigException ex = new ConfigException(
- "Unable to create backup file [" + configBackupFile.toString() + "]");
- log.error(ex.getMessage(), ex);
- }
-
- try {
- /*
- * write config file
- */
- Files.write(configFile, config.getBytes(DEFAULT_CHARSET));
- } catch (IOException e) {
- ConfigException ex = new ConfigException(
- "Unable to write config file [" + configFile.toString() + "]", e);
- log.error(ex.getMessage(), ex);
-
- try {
- /*
- * On error: recover backup file
- */
- Files.copy(configBackupFile, configFile, StandardCopyOption.REPLACE_EXISTING);
- } catch (IOException e2) {
- ConfigException ex2 = new ConfigException(
- "Unable to recover backup file [" + configBackupFile.toString() + "]");
- log.error(ex.getMessage(), ex);
- }
- }
- }
- };
- this.writeConfigExecutor.execute(writeConfigRunnable);
- }
-
- public synchronized void parseJsonConfig(JsonObject jConfig)
- throws ReflectionException, ConfigException, WriteChannelException {
- /*
- * read Users
- */
- if (jConfig.has("users")) {
- JsonObject jUsers = JsonUtils.getAsJsonObject(jConfig, "users");
- for (Entry jUsersElement : jUsers.entrySet()) {
- JsonObject jUser = JsonUtils.getAsJsonObject(jUsersElement.getValue());
- String username = jUsersElement.getKey();
- String passwordBase64 = JsonUtils.getAsString(jUser, "password");
- String saltBase64 = JsonUtils.getAsString(jUser, "salt");
- try {
- User.getUserByName(username).initialize(passwordBase64, saltBase64);
- } catch (OpenemsException e) {
- log.error("Error parsing config: " + e.getMessage());
- }
- }
- }
- User.initializeFinished(); // important! no more setting of users allowed!
-
- /*
- * read each Bridge in "things" array
- */
- JsonArray jThings = JsonUtils.getAsJsonArray(jConfig, "things");
- for (JsonElement jBridgeElement : jThings) {
- JsonObject jBridge = JsonUtils.getAsJsonObject(jBridgeElement);
- String bridgeClass = JsonUtils.getAsString(jBridge, "class");
- Bridge bridge = (Bridge) InjectionUtils.getThingInstance(bridgeClass);
- thingRepository.addThing(bridge);
- log.debug("Add Bridge[" + bridge.id() + "], Implementation[" + bridge.getClass().getSimpleName() + "]");
- ConfigUtils.injectConfigChannels(thingRepository.getConfigChannels(bridge), jBridge);
- /*
- * read each Device in "things" array
- */
- List devices = new ArrayList<>();
- JsonArray jDevices = JsonUtils.getAsJsonArray(jBridge, "devices");
- for (JsonElement jDeviceElement : jDevices) {
- JsonObject jDevice = JsonUtils.getAsJsonObject(jDeviceElement);
- Device device = thingRepository.createDevice(jDevice);
- devices.add(device);
- bridge.addDevice(device);
- }
- }
-
- /*
- * read Scheduler
- */
- if (jConfig.has("scheduler")) {
- JsonObject jScheduler = JsonUtils.getAsJsonObject(jConfig, "scheduler");
- String schedulerClass = JsonUtils.getAsString(jScheduler, "class");
- Scheduler scheduler = (Scheduler) InjectionUtils.getThingInstance(schedulerClass);
- thingRepository.addThing(scheduler);
- log.debug("Add Scheduler[" + scheduler.id() + "], Implementation[" + scheduler.getClass().getSimpleName()
- + "]");
- ConfigUtils.injectConfigChannels(thingRepository.getConfigChannels(scheduler), jScheduler);
- /*
- * read each Controller in "controllers" array
- */
- JsonArray jControllers = JsonUtils.getAsJsonArray(jScheduler, "controllers");
- for (JsonElement jControllerElement : jControllers) {
- JsonObject jController = JsonUtils.getAsJsonObject(jControllerElement);
- Controller controller = thingRepository.createController(jController);
- scheduler.addController(controller);
- }
- }
-
- /*
- * read Persistence
- */
- if (jConfig.has("persistence")) {
- JsonArray jPersistences = JsonUtils.getAsJsonArray(jConfig, "persistence");
- for (JsonElement jPersistenceElement : jPersistences) {
- JsonObject jPersistence = JsonUtils.getAsJsonObject(jPersistenceElement);
- String persistenceClass = JsonUtils.getAsString(jPersistence, "class");
- Persistence persistence = (Persistence) InjectionUtils.getThingInstance(persistenceClass);
- thingRepository.addThing(persistence);
- log.debug("Add Persistence[" + persistence.id() + "], Implementation["
- + persistence.getClass().getSimpleName() + "]");
- ConfigUtils.injectConfigChannels(thingRepository.getConfigChannels(persistence), jPersistence);
- }
- }
-
- /*
- * Configuration is finished -> start all worker threads
- */
- thingRepository.getThings().forEach(thing -> {
- if (thing instanceof Thread) {
- ((AbstractWorker) thing).start();
- }
- });
-
- /*
- * Register myself as onChangeListener on all ConfigChannels
- */
- for (ConfigChannel> channel : thingRepository.getConfigChannels()) {
- channel.addChangeListener(this);
- }
- }
-
- public JsonArray getBridgesJson(boolean includeEverything) throws NotImplementedException {
- JsonArray jBridges = new JsonArray();
- for (Bridge bridge : thingRepository.getBridges()) {
- JsonObject jBridge = (JsonObject) ConfigUtils.getAsJsonElement(bridge, includeEverything);
- /*
- * Device
- */
- JsonArray jDevices = new JsonArray();
- for (Device device : bridge.getDevices()) {
- JsonObject jDevice = (JsonObject) ConfigUtils.getAsJsonElement(device, includeEverything);
- jDevices.add(jDevice);
- }
- jBridge.add("devices", jDevices);
- jBridges.add(jBridge);
- }
- return jBridges;
- }
-
- public JsonObject getSchedulerJson(boolean includeEverything) throws NotImplementedException {
- JsonObject jScheduler = null;
- for (Scheduler scheduler : thingRepository.getSchedulers()) {
- jScheduler = (JsonObject) ConfigUtils.getAsJsonElement(scheduler, includeEverything);
- /*
- * Controller
- */
- JsonArray jControllers = new JsonArray();
- for (Controller controller : scheduler.getControllers()) {
- jControllers.add(ConfigUtils.getAsJsonElement(controller, includeEverything));
- }
- jScheduler.add("controllers", jControllers);
- break; // TODO only one Scheduler supported
- }
- return jScheduler;
- }
-
- public JsonArray getPersistenceJson(boolean includeEverything) throws NotImplementedException {
- JsonArray jPersistences = new JsonArray();
- for (Persistence persistence : thingRepository.getPersistences()) {
- JsonObject jPersistence = (JsonObject) ConfigUtils.getAsJsonElement(persistence, includeEverything);
- jPersistences.add(jPersistence);
- }
- return jPersistences;
- }
-
- private JsonObject getUsersJson() {
- JsonObject jUsers = new JsonObject();
- for (User user : User.getUsers()) {
- JsonObject jUser = new JsonObject();
- jUser.addProperty("password", user.getPasswordBase64());
- jUser.addProperty("salt", user.getSaltBase64());
- jUsers.add(user.getName(), jUser);
- }
- return jUsers;
- }
-
- public synchronized JsonObject getJson(boolean includeEverything) throws NotImplementedException {
- JsonObject jConfig = new JsonObject();
- /*
- * Bridge
- */
- jConfig.add("things", getBridgesJson(includeEverything));
- /*
- * Scheduler
- */
- jConfig.add("scheduler", getSchedulerJson(includeEverything));
- /*
- * Persistence
- */
- jConfig.add("persistence", getPersistenceJson(includeEverything));
- return jConfig;
- }
-
- private synchronized JsonObject getJsonComplete() throws NotImplementedException {
- JsonObject jConfig = getJson(false);
- /*
- * Users
- */
- jConfig.add("users", getUsersJson());
- return jConfig;
- }
-
- /**
- * Receives update events for config channels and rewrites the json config
- */
- @Override
- public void channelChanged(Channel channel, Optional> newValue, Optional> oldValue) {
- // TODO: trigger ConfigUpdated event
- try {
- writeConfigFile();
- } catch (OpenemsException e) {
- log.error("Config-Error.", e);
- }
- }
-
- /**
- * Provides the File path of the config file ("/etc/openems.d/config.json") or a local file on a development machine
- */
- private Path getConfigFile() {
- String relativePath = CONFIG_PATH + "/" + CONFIG_FILE_NAME;
- Path configFile = Paths.get(relativePath);
- if (Files.isReadable(configFile)) {
- // we are on development system
- return configFile;
- }
- return Paths.get("/" + relativePath);
- }
-
- /**
- * Provides the File path of the config backup file ("/etc/openems.d/config.backup.json") or a local file on a
- * development machine
- */
- private Path getConfigBackupFile() {
- Path configFile = getConfigFile();
- Path backupFile = configFile.getParent().resolve(CONFIG_BACKUP_FILE_NAME);
- return backupFile;
- }
-
- /**
- * Generates a JsonObject including the current configuration as well as meta-data about available controllers,
- * bridges,...
- *
- * @return
- */
- public JsonObject getMetaConfigJson() {
- try {
- /*
- * Json Config
- */
- JsonObject j = getJson(true);
- JsonObject jMeta = new JsonObject();
- /*
- * Devices -> Natures
- */
- JsonObject jDeviceNatures = new JsonObject();
- thingRepository.getDeviceNatures().forEach(nature -> {
- JsonArray jNatureImplements = new JsonArray();
- /*
- * get important classes/interfaces that are implemented by this nature
- */
- for (Class> iface : InjectionUtils.getImportantNatureInterfaces(nature.getClass())) {
- jNatureImplements.add(iface.getSimpleName());
- }
- JsonObject jDeviceNature = new JsonObject();
- jDeviceNature.add("implements", jNatureImplements);
- JsonObject jChannels = new JsonObject();
- thingRepository.getConfigChannels(nature).forEach(channel -> {
- try {
- jChannels.add(channel.id(), channel.toJsonObject());
- } catch (NotImplementedException e) {
- /* ignore */
- }
- });
- jDeviceNature.add("channels", jChannels);
- jDeviceNatures.add(nature.id(), jDeviceNature);
- });
- jMeta.add("natures", jDeviceNatures);
- /*
- * Available
- */
- ClassRepository classRepository = ClassRepository.getInstance();
- // Controllers
- JsonObject jAvailableControllers = new JsonObject();
- for (ThingDoc description : classRepository.getAvailableControllers()) {
- jAvailableControllers.add(description.getClazz().getName(), description.getAsJsonObject());
- }
- jMeta.add("availableControllers", jAvailableControllers);
- // Bridges
- JsonObject jAvailableBridges = new JsonObject();
- for (ThingDoc description : classRepository.getAvailableBridges()) {
- jAvailableBridges.add(description.getClazz().getName(), description.getAsJsonObject());
- }
- jMeta.add("availableBridges", jAvailableBridges);
- // Devices
- JsonObject jAvailableDevices = new JsonObject();
- for (ThingDoc description : classRepository.getAvailableDevices()) {
- jAvailableDevices.add(description.getClazz().getName(), description.getAsJsonObject());
- }
- jMeta.add("availableDevices", jAvailableDevices);
- // Schedulers
- JsonObject jAvailableSchedulers = new JsonObject();
- for (ThingDoc description : classRepository.getAvailableSchedulers()) {
- jAvailableSchedulers.add(description.getClazz().getName(), description.getAsJsonObject());
- }
- jMeta.add("availableSchedulers", jAvailableSchedulers);
- j.add("_meta", jMeta);
- return j;
- } catch (NotImplementedException | ReflectionException e) {
- log.warn("Unable to create config: " + e.getMessage());
- return new JsonObject();
- }
- }
-}
+/*******************************************************************************
+ * 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.core;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import io.openems.api.bridge.Bridge;
+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.Device;
+import io.openems.api.doc.ThingDoc;
+import io.openems.api.exception.ConfigException;
+import io.openems.api.exception.NotImplementedException;
+import io.openems.api.exception.OpenemsException;
+import io.openems.api.exception.ReflectionException;
+import io.openems.api.exception.WriteChannelException;
+import io.openems.api.persistence.Persistence;
+import io.openems.api.scheduler.Scheduler;
+import io.openems.api.security.User;
+import io.openems.api.thing.Thing;
+import io.openems.core.utilities.ConfigUtils;
+import io.openems.core.utilities.InjectionUtils;
+import io.openems.core.utilities.JsonUtils;
+
+public class Config implements ChannelChangeListener {
+
+ private final static Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+ private final static String CONFIG_PATH = "etc/openems.d";
+ private final static String CONFIG_FILE_NAME = "config.json";
+ private final static String CONFIG_BACKUP_FILE_NAME = "config.backup.json";
+
+ private final static Logger log = LoggerFactory.getLogger(Config.class);
+ private static Config instance;
+
+ public static synchronized Config getInstance() throws ConfigException {
+ if (Config.instance == null) {
+ Config.instance = new Config();
+ }
+ return Config.instance;
+ }
+
+ private final ThingRepository thingRepository;
+ private final Path configFile;
+ private final Path configBackupFile;
+ private final ExecutorService writeConfigExecutor;
+
+ public Config() throws ConfigException {
+ thingRepository = ThingRepository.getInstance();
+ this.configFile = getConfigFile();
+ this.configBackupFile = getConfigBackupFile();
+ this.writeConfigExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ public synchronized void readConfigFile() throws Exception {
+ // Read configuration from default config file
+ try {
+ readConfigFromFile(configFile);
+ log.info("Read configuration from file [" + configFile.toString() + "]");
+ return;
+ } catch (Exception e) {
+ log.warn("Failed to read configuration from file [" + configFile.toString() + "] ", e);
+ }
+ // Read configuration from backup config file
+ try {
+ readConfigFromFile(configBackupFile);
+ log.info("Read configuration from backup file [" + configBackupFile.toString() + "]");
+ } catch (Exception e) {
+ log.warn("Failed to read configuration backup file [" + configFile.toString() + "]", e);
+ throw e;
+ }
+ }
+
+ private synchronized void readConfigFromFile(Path path) throws Exception {
+ JsonObject jConfig = new JsonObject();
+ JsonParser parser = new JsonParser();
+ String config = new String(Files.readAllBytes(path), DEFAULT_CHARSET);
+ JsonElement jsonElement = parser.parse(config);
+ jConfig = jsonElement.getAsJsonObject();
+ jConfig = addDefaultConfig(jConfig);
+ // apply config
+ parseJsonConfig(jConfig);
+ }
+
+ private JsonObject addDefaultConfig(JsonObject jConfig) {
+ try {
+ /*
+ * Things
+ */
+ JsonArray jThings;
+ if (!jConfig.has("things") || !jConfig.get("things").isJsonArray()) {
+ jThings = new JsonArray();
+ } else {
+ jThings = JsonUtils.getAsJsonArray(jConfig, "things");
+ }
+ {
+ /*
+ * Add SystemBridge
+ */
+ if (!JsonUtils.hasElement(jConfig, "things", "class", "io.openems.impl.protocol.system.SystemBridge")) {
+ JsonObject jBridge = new JsonObject();
+ {
+ jBridge.addProperty("class", "io.openems.impl.protocol.system.SystemBridge");
+ JsonArray jDevices = new JsonArray();
+ {
+ JsonObject jSystem = new JsonObject();
+ jSystem.addProperty("class", "io.openems.impl.device.system.System");
+ {
+ JsonObject jSystemNature = new JsonObject();
+ {
+ jSystemNature.addProperty("id", "system0");
+ }
+ jSystem.add("system", jSystemNature);
+ }
+ jDevices.add(jSystem);
+ }
+ jBridge.add("devices", jDevices);
+ }
+ jThings.add(jBridge);
+ }
+ }
+ jConfig.add("things", jThings);
+ /*
+ * Scheduler
+ */
+ JsonObject jScheduler;
+ if (!jConfig.has("scheduler") || !jConfig.get("scheduler").isJsonObject()) {
+ jScheduler = new JsonObject();
+ jScheduler.addProperty("class", "io.openems.impl.scheduler.SimpleScheduler");
+ } else {
+ jScheduler = JsonUtils.getAsJsonObject(jConfig, "scheduler");
+ }
+ {
+ /*
+ * Controller
+ */
+ JsonArray jControllers;
+ if (!jScheduler.has("controllers") || !jScheduler.get("controllers").isJsonArray()) {
+ jControllers = new JsonArray();
+ } else {
+ jControllers = JsonUtils.getAsJsonArray(jScheduler, "controllers");
+ }
+ {
+ /*
+ * WebsocketApiController
+ */
+ if (!JsonUtils.hasElement(jControllers, "class",
+ "io.openems.impl.controller.api.websocket.WebsocketApiController")) {
+ JsonObject jWebsocketApiController = new JsonObject();
+ jWebsocketApiController.addProperty("class",
+ "io.openems.impl.controller.api.websocket.WebsocketApiController");
+ jWebsocketApiController.addProperty("priority", Integer.MIN_VALUE);
+ jControllers.add(jWebsocketApiController);
+ }
+ /*
+ * RestApiController
+ */
+ if (!JsonUtils.hasElement(jControllers, "class",
+ "io.openems.impl.controller.api.rest.RestApiController")) {
+ JsonObject jRestApiController = new JsonObject();
+ jRestApiController.addProperty("class",
+ "io.openems.impl.controller.api.rest.RestApiController");
+ jRestApiController.addProperty("priority", Integer.MIN_VALUE);
+ jControllers.add(jRestApiController);
+ }
+ }
+ jScheduler.add("controllers", jControllers);
+ }
+ jConfig.add("scheduler", jScheduler);
+ } catch (ReflectionException e) {
+ log.warn("Error applying default config: " + e.getMessage());
+ }
+ return jConfig;
+ }
+
+ /**
+ * Writes the config file. Holds a backup config file and restores it on error. Method is executed asynchronously.
+ *
+ * @throws NotImplementedException
+ */
+ public void writeConfigFile() throws NotImplementedException {
+ // get config as json
+ JsonObject jConfig = getJson(ConfigFormat.FILE);
+
+ Runnable writeConfigRunnable = new Runnable() {
+ @Override
+ public void run() {
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ String config = gson.toJson(jConfig);
+ try {
+ /*
+ * create backup of config file
+ */
+ Files.copy(configFile, configBackupFile, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ ConfigException ex = new ConfigException(
+ "Unable to create backup file [" + configBackupFile.toString() + "]");
+ log.error(ex.getMessage(), ex);
+ }
+
+ try {
+ /*
+ * write config file
+ */
+ Files.write(configFile, config.getBytes(DEFAULT_CHARSET));
+ } catch (IOException e) {
+ ConfigException ex = new ConfigException(
+ "Unable to write config file [" + configFile.toString() + "]", e);
+ log.error(ex.getMessage(), ex);
+
+ try {
+ /*
+ * On error: recover backup file
+ */
+ Files.copy(configBackupFile, configFile, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e2) {
+ ConfigException ex2 = new ConfigException(
+ "Unable to recover backup file [" + configBackupFile.toString() + "]");
+ log.error(ex.getMessage(), ex);
+ }
+ }
+ }
+ };
+ this.writeConfigExecutor.execute(writeConfigRunnable);
+ }
+
+ public synchronized void parseJsonConfig(JsonObject jConfig)
+ throws ReflectionException, ConfigException, WriteChannelException {
+ /*
+ * read Users
+ */
+ if (jConfig.has("users")) {
+ JsonObject jUsers = JsonUtils.getAsJsonObject(jConfig, "users");
+ for (Entry jUsersElement : jUsers.entrySet()) {
+ JsonObject jUser = JsonUtils.getAsJsonObject(jUsersElement.getValue());
+ String username = jUsersElement.getKey();
+ String passwordBase64 = JsonUtils.getAsString(jUser, "password");
+ String saltBase64 = JsonUtils.getAsString(jUser, "salt");
+ try {
+ User.getUserByName(username).initialize(passwordBase64, saltBase64);
+ } catch (OpenemsException e) {
+ log.error("Error parsing config: " + e.getMessage());
+ }
+ }
+ }
+ User.initializeFinished(); // important! no more setting of users allowed!
+
+ /*
+ * read each Bridge in "things" array
+ */
+ JsonArray jThings = JsonUtils.getAsJsonArray(jConfig, "things");
+ for (JsonElement jBridgeElement : jThings) {
+ JsonObject jBridge = JsonUtils.getAsJsonObject(jBridgeElement);
+ String bridgeClass = JsonUtils.getAsString(jBridge, "class");
+ Bridge bridge = (Bridge) InjectionUtils.getThingInstance(bridgeClass);
+ thingRepository.addThing(bridge);
+ log.debug("Add Bridge[" + bridge.id() + "], Implementation[" + bridge.getClass().getSimpleName() + "]");
+ ConfigUtils.injectConfigChannels(thingRepository.getConfigChannels(bridge), jBridge);
+ /*
+ * read each Device in "things" array
+ */
+ List devices = new ArrayList<>();
+ JsonArray jDevices = JsonUtils.getAsJsonArray(jBridge, "devices");
+ for (JsonElement jDeviceElement : jDevices) {
+ JsonObject jDevice = JsonUtils.getAsJsonObject(jDeviceElement);
+ Device device = thingRepository.createDevice(jDevice, bridge);
+ devices.add(device);
+ bridge.addDevice(device);
+ }
+ /*
+ * Init bridge
+ */
+ bridge.init();
+ for (Device d : bridge.getDevices()) {
+ d.init();
+ }
+ }
+
+ /*
+ * read Scheduler
+ */
+ if (jConfig.has("scheduler")) {
+ JsonObject jScheduler = JsonUtils.getAsJsonObject(jConfig, "scheduler");
+ String schedulerClass = JsonUtils.getAsString(jScheduler, "class");
+ Scheduler scheduler = (Scheduler) InjectionUtils.getThingInstance(schedulerClass);
+ thingRepository.addThing(scheduler);
+ log.debug("Add Scheduler[" + scheduler.id() + "], Implementation[" + scheduler.getClass().getSimpleName()
+ + "]");
+ ConfigUtils.injectConfigChannels(thingRepository.getConfigChannels(scheduler), jScheduler);
+ /*
+ * read each Controller in "controllers" array
+ */
+ JsonArray jControllers = JsonUtils.getAsJsonArray(jScheduler, "controllers");
+ for (JsonElement jControllerElement : jControllers) {
+ JsonObject jController = JsonUtils.getAsJsonObject(jControllerElement);
+ Controller controller = thingRepository.createController(jController);
+ scheduler.addController(controller);
+ controller.init();
+ }
+ scheduler.init();
+ }
+
+ /*
+ * read Persistence
+ */
+ if (jConfig.has("persistence")) {
+ JsonArray jPersistences = JsonUtils.getAsJsonArray(jConfig, "persistence");
+ for (JsonElement jPersistenceElement : jPersistences) {
+ JsonObject jPersistence = JsonUtils.getAsJsonObject(jPersistenceElement);
+ String persistenceClass = JsonUtils.getAsString(jPersistence, "class");
+ Persistence persistence = (Persistence) InjectionUtils.getThingInstance(persistenceClass);
+ thingRepository.addThing(persistence);
+ log.debug("Add Persistence[" + persistence.id() + "], Implementation["
+ + persistence.getClass().getSimpleName() + "]");
+ ConfigUtils.injectConfigChannels(thingRepository.getConfigChannels(persistence), jPersistence);
+ persistence.init();
+ }
+ }
+
+ /*
+ * Configuration is finished -> start all worker threads
+ */
+ thingRepository.getThings().forEach(thing -> {
+ if (thing instanceof Thread) {
+ ((Thread) thing).start();
+ }
+ });
+
+ /*
+ * Register myself as onChangeListener on all ConfigChannels
+ */
+ for (ConfigChannel> channel : thingRepository.getConfigChannels()) {
+ channel.addChangeListener(this);
+ }
+
+ /*
+ * After 10 seconds: build the ClassRepository cache to speed up future calls
+ * (this speeds up the first opening of the UI, as the cache does not need to be built)
+ */
+ Executors.newScheduledThreadPool(1).schedule(() -> {
+ try {
+ ClassRepository.getInstance().getAvailableThings();
+ } catch (ReflectionException e) { /* ignore */}
+ }, 10, TimeUnit.SECONDS);
+ }
+
+ public JsonArray getBridgesJson(ConfigFormat format) throws NotImplementedException {
+ JsonArray jBridges = new JsonArray();
+ for (Bridge bridge : thingRepository.getBridges()) {
+ JsonObject jBridge = (JsonObject) ConfigUtils.getAsJsonElement(bridge, format);
+ /*
+ * Device
+ */
+ JsonArray jDevices = new JsonArray();
+ for (Device device : bridge.getDevices()) {
+ JsonObject jDevice = (JsonObject) ConfigUtils.getAsJsonElement(device, format);
+ jDevices.add(jDevice);
+ }
+ jBridge.add("devices", jDevices);
+ jBridges.add(jBridge);
+ }
+ return jBridges;
+ }
+
+ public JsonObject getSchedulerJson(ConfigFormat format) throws NotImplementedException {
+ JsonObject jScheduler = null;
+ for (Scheduler scheduler : thingRepository.getSchedulers()) {
+ jScheduler = (JsonObject) ConfigUtils.getAsJsonElement(scheduler, format);
+ /*
+ * Controller
+ */
+ JsonArray jControllers = new JsonArray();
+ for (Controller controller : scheduler.getControllers()) {
+ jControllers.add(ConfigUtils.getAsJsonElement(controller, format));
+ }
+ jScheduler.add("controllers", jControllers);
+ break; // TODO only one Scheduler supported
+ }
+ return jScheduler;
+ }
+
+ public JsonArray getPersistenceJson(ConfigFormat format) throws NotImplementedException {
+ JsonArray jPersistences = new JsonArray();
+ for (Persistence persistence : thingRepository.getPersistences()) {
+ JsonObject jPersistence = (JsonObject) ConfigUtils.getAsJsonElement(persistence, format);
+ jPersistences.add(jPersistence);
+ }
+ return jPersistences;
+ }
+
+ private JsonObject getUsersJson() {
+ JsonObject jUsers = new JsonObject();
+ for (User user : User.getUsers()) {
+ JsonObject jUser = new JsonObject();
+ jUser.addProperty("password", user.getPasswordBase64());
+ jUser.addProperty("salt", user.getSaltBase64());
+ jUsers.add(user.getName(), jUser);
+ }
+ return jUsers;
+ }
+
+ /**
+ * Gets the Config as Json in the given format in english language
+ *
+ * @param format
+ * @return
+ * @throws NotImplementedException
+ */
+ public synchronized JsonObject getJson(ConfigFormat format) throws NotImplementedException {
+ return this.getJson(format, "en"); // TODO use proper language tag
+ }
+
+ /**
+ * Gets the Config as Json in the given format
+ *
+ * @param format
+ * @return
+ * @throws NotImplementedException
+ */
+ // TODO make use of language tag
+ public synchronized JsonObject getJson(ConfigFormat format, String language) throws NotImplementedException {
+ JsonObject jConfig = new JsonObject();
+ if (format == ConfigFormat.FILE) {
+ /*
+ * Prepare Json in format for config.json file
+ */
+ // Bridge
+ jConfig.add("things", getBridgesJson(format));
+ // Scheduler
+ jConfig.add("scheduler", getSchedulerJson(format));
+ // Persistence
+ jConfig.add("persistence", getPersistenceJson(format));
+ // Users
+ jConfig.add("users", getUsersJson());
+
+ } else {
+ /*
+ * Prepare Json in format for OpenEMS UI
+ */
+ // things...
+ JsonObject jThings = new JsonObject();
+ Set things = ThingRepository.getInstance().getThings();
+ for (Thing thing : things) {
+ JsonObject jThing = (JsonObject) ConfigUtils.getAsJsonElement(thing, format);
+ jThings.add(thing.id(), jThing);
+ }
+ jConfig.add("things", jThings);
+ // meta...
+ JsonObject jMeta = new JsonObject();
+ try {
+ Iterable availableThings = ClassRepository.getInstance().getAvailableThings();
+ for (ThingDoc availableThing : availableThings) {
+ jMeta.add(availableThing.getClazz().getName(), availableThing.getAsJsonObject());
+ }
+ } catch (ReflectionException e) {
+ log.error(e.getMessage());
+ e.printStackTrace();
+ }
+ jConfig.add("meta", jMeta);
+ }
+ return jConfig;
+ }
+
+ /**
+ * Receives update events for config channels and rewrites the json config
+ */
+ @Override
+ public void channelChanged(Channel channel, Optional> newValue, Optional> oldValue) {
+ // TODO: trigger ConfigUpdated event
+ try {
+ writeConfigFile();
+ } catch (OpenemsException e) {
+ log.error("Config-Error.", e);
+ }
+ }
+
+ /**
+ * Provides the File path of the config file ("/etc/openems.d/config.json") or a local file on a development machine
+ */
+ private Path getConfigFile() {
+ String relativePath = CONFIG_PATH + "/" + CONFIG_FILE_NAME;
+ Path configFile = Paths.get(relativePath);
+ if (Files.isReadable(configFile)) {
+ // we are on development system
+ return configFile;
+ }
+ return Paths.get("/" + relativePath);
+ }
+
+ /**
+ * Provides the File path of the config backup file ("/etc/openems.d/config.backup.json") or a local file on a
+ * development machine
+ */
+ private Path getConfigBackupFile() {
+ Path configFile = getConfigFile();
+ Path backupFile = configFile.getParent().resolve(CONFIG_BACKUP_FILE_NAME);
+ return backupFile;
+ }
+}
\ No newline at end of file
diff --git a/edge/src/io/openems/core/ConfigFormat.java b/edge/src/io/openems/core/ConfigFormat.java
new file mode 100644
index 00000000000..d87ec33e4a7
--- /dev/null
+++ b/edge/src/io/openems/core/ConfigFormat.java
@@ -0,0 +1,10 @@
+package io.openems.core;
+
+/**
+ * Describes a format for JSON configuration output
+ *
+ * @author stefan.feilmeier
+ */
+public enum ConfigFormat {
+ FILE, OPENEMS_UI
+}
diff --git a/edge/src/io/openems/core/ThingRepository.java b/edge/src/io/openems/core/ThingRepository.java
index f9621e76e86..f5de9965104 100644
--- a/edge/src/io/openems/core/ThingRepository.java
+++ b/edge/src/io/openems/core/ThingRepository.java
@@ -1,478 +1,480 @@
-/*******************************************************************************
- * 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.core;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Member;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.HashBiMap;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Table;
-import com.google.gson.JsonObject;
-
-import io.openems.api.bridge.Bridge;
-import io.openems.api.channel.Channel;
-import io.openems.api.channel.ConfigChannel;
-import io.openems.api.channel.ReadChannel;
-import io.openems.api.channel.WriteChannel;
-import io.openems.api.controller.Controller;
-import io.openems.api.device.Device;
-import io.openems.api.device.nature.DeviceNature;
-import io.openems.api.exception.ReflectionException;
-import io.openems.api.persistence.Persistence;
-import io.openems.api.persistence.QueryablePersistence;
-import io.openems.api.scheduler.Scheduler;
-import io.openems.api.thing.Thing;
-import io.openems.api.thing.ThingChannelsUpdatedListener;
-import io.openems.core.ThingsChangedListener.Action;
-import io.openems.core.utilities.ConfigUtils;
-import io.openems.core.utilities.InjectionUtils;
-import io.openems.core.utilities.JsonUtils;
-
-public class ThingRepository implements ThingChannelsUpdatedListener {
- private final static Logger log = LoggerFactory.getLogger(ThingRepository.class);
-
- private static ThingRepository instance;
-
- public static synchronized ThingRepository getInstance() {
- if (ThingRepository.instance == null) {
- ThingRepository.instance = new ThingRepository();
- }
- return ThingRepository.instance;
- }
-
- private ThingRepository() {
- classRepository = ClassRepository.getInstance();
- }
-
- private final ClassRepository classRepository;
- private final BiMap thingIds = HashBiMap.create();
- private HashMultimap, Thing> thingClasses = HashMultimap.create();
- private Set bridges = new HashSet<>();
- private Set schedulers = new HashSet<>();
- private Set persistences = new HashSet<>();
- private Set queryablePersistences = new HashSet<>();
- private Set deviceNatures = new HashSet<>();
- private final Table thingChannels = HashBasedTable.create();
- private HashMultimap