[semsportal] initial contribution (#10100)

Signed-off-by: Iwan Bron <bron@olisa.eu>
This commit is contained in:
Iwan Bron
2021-05-23 20:45:04 +02:00
committed by GitHub
parent 4b5d64340f
commit 66a540289d
36 changed files with 2817 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.semsportal-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-semsportal" description="SEMSPortal Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.semsportal/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception indicating there was a problem communicating with the portal. It can indicate either no response at all, or
* a response that was not expected.
*
* @author Iwan Bron - Initial contribution
*
*/
@NonNullByDefault
public class CommunicationException extends Exception {
private static final long serialVersionUID = 4175625868879971138L;
public CommunicationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception indicating that the configuration of the portal was wrong, like an unknown account or invalid password.
*
* @author Iwan Bron - Initial contribution
*
*/
@NonNullByDefault
public class ConfigurationException extends Exception {
private static final long serialVersionUID = -803416460838670618L;
public ConfigurationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,229 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.semsportal.internal.dto.BaseResponse;
import org.openhab.binding.semsportal.internal.dto.LoginRequest;
import org.openhab.binding.semsportal.internal.dto.LoginResponse;
import org.openhab.binding.semsportal.internal.dto.SEMSToken;
import org.openhab.binding.semsportal.internal.dto.Station;
import org.openhab.binding.semsportal.internal.dto.StationListRequest;
import org.openhab.binding.semsportal.internal.dto.StationListResponse;
import org.openhab.binding.semsportal.internal.dto.StationStatus;
import org.openhab.binding.semsportal.internal.dto.StatusRequest;
import org.openhab.binding.semsportal.internal.dto.StatusResponse;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link PortalHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class PortalHandler extends BaseBridgeHandler {
private Logger logger = LoggerFactory.getLogger(PortalHandler.class);
// the settings that are needed when you do not have avalid session token yet
private static final SEMSToken SESSIONLESS_TOKEN = new SEMSToken("v2.1.0", "ios", "en");
// the url of the SEMS portal API
private static final String BASE_URL = "https://www.semsportal.com/";
// url for the login request, to get a valid session token
private static final String LOGIN_URL = BASE_URL + "api/v2/Common/CrossLogin";
// url to get the status of a specific power station
private static final String STATUS_URL = BASE_URL + "api/v2/PowerStation/GetMonitorDetailByPowerstationId";
private static final String LIST_URL = BASE_URL + "api/PowerStationMonitor/QueryPowerStationMonitorForApp";
// the token holds the credential information for the portal
private static final String HTTP_HEADER_TOKEN = "Token";
// used to parse json from / to the SEMS portal API
private final Gson gson;
private final HttpClient httpClient;
// configuration as provided by the openhab framework: initialize with defaults to prevent compiler check errors
private SEMSPortalConfiguration config = new SEMSPortalConfiguration();
private boolean loggedIn;
private SEMSToken sessionToken = SESSIONLESS_TOKEN;// gets the default, it is needed for the login
private @Nullable StationStatus currentStatus;
private @Nullable ScheduledFuture<?> pollingJob;
public PortalHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
super(bridge);
httpClient = httpClientFactory.getCommonHttpClient();
gson = new GsonBuilder().setDateFormat(SEMSPortalBindingConstants.DATE_FORMAT).create();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("No supported commands. Ignoring command {} for channel {}", command, channelUID);
return;
}
@Override
public void initialize() {
config = getConfigAs(SEMSPortalConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> {
try {
login();
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
"Error when loggin in. Check your username and password");
}
});
}
@Override
public void dispose() {
ScheduledFuture<?> localPollingJob = pollingJob;
if (localPollingJob != null) {
localPollingJob.cancel(true);
}
super.dispose();
}
private void login() {
loggedIn = false;
String payload = gson.toJson(new LoginRequest(config.username, config.password));
String response = sendPost(LOGIN_URL, payload);
if (response == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid response from SEMS portal");
return;
}
LoginResponse loginResponse = gson.fromJson(response, LoginResponse.class);
if (loginResponse == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check username / password");
return;
}
if (loginResponse.isOk()) {
logger.debug("Successfuly logged in to SEMS portal");
if (loginResponse.getToken() != null) {
sessionToken = loginResponse.getToken();
}
loggedIn = true;
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.CONFIGURATION_ERROR, "Check username / password");
}
}
private @Nullable String sendPost(String url, String payload) {
try {
Request request = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.header(HTTP_HEADER_TOKEN, gson.toJson(sessionToken))
.content(new StringContentProvider(payload, StandardCharsets.UTF_8.name()),
MediaType.APPLICATION_JSON);
request.getHeaders().remove(HttpHeader.ACCEPT_ENCODING);
ContentResponse response = request.send();
logger.trace("received response: {}", response.getContentAsString());
return response.getContentAsString();
} catch (Exception e) {
logger.debug("{} when posting to url {}", e.getClass().getSimpleName(), url, e);
}
return null;
}
public boolean isLoggedIn() {
return loggedIn;
}
public @Nullable StationStatus getStationStatus(String stationUUID)
throws CommunicationException, ConfigurationException {
if (!loggedIn) {
logger.debug("Not logged in. Not updating.");
return null;
}
String response = sendPost(STATUS_URL, gson.toJson(new StatusRequest(stationUUID)));
if (response == null) {
throw new CommunicationException("No response received from portal");
}
BaseResponse semsResponse = gson.fromJson(response, BaseResponse.class);
if (semsResponse == null) {
throw new CommunicationException("Portal reponse not understood");
}
if (semsResponse.isOk()) {
StatusResponse statusResponse = gson.fromJson(response, StatusResponse.class);
if (statusResponse == null) {
throw new CommunicationException("Portal reponse not understood");
}
currentStatus = statusResponse.getStatus();
updateStatus(ThingStatus.ONLINE); // we got a valid response, register as online
return currentStatus;
} else if (semsResponse.isSessionInvalid()) {
logger.debug("Session is invalidated. Attempting new login.");
login();
return getStationStatus(stationUUID);
} else if (semsResponse.isError()) {
throw new ConfigurationException(
"ERROR status code received from SEMS portal. Please check your station ID");
} else {
throw new CommunicationException(String.format("Unknown status code received from SEMS portal: %s - %s",
semsResponse.getCode(), semsResponse.getMsg()));
}
}
public long getUpdateInterval() {
return config.update;
}
public List<Station> getAllStations() {
String payload = gson.toJson(new StationListRequest());
String response = sendPost(LIST_URL, payload);
if (response == null) {
logger.debug("Received empty list stations response from SEMS portal");
return Collections.emptyList();
}
StationListResponse listResponse = gson.fromJson(response, StationListResponse.class);
if (listResponse == null) {
logger.debug("Unable to read list stations response from SEMS portal");
return Collections.emptyList();
}
if (listResponse.isOk()) {
logger.debug("Received list of {} stations from SEMS portal", listResponse.getStations().size());
loggedIn = true;
updateStatus(ThingStatus.ONLINE);
return listResponse.getStations();
} else {
logger.debug("Received error response with code {} and message {} from SEMS portal", listResponse.getCode(),
listResponse.getMsg());
return Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SEMSPortalBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class SEMSPortalBindingConstants {
private static final String BINDING_ID = "semsportal";
static final String DATE_FORMAT = "MM.dd.yyyy HH:mm:ss";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_PORTAL = new ThingTypeUID(BINDING_ID, "portal");
public static final ThingTypeUID THING_TYPE_STATION = new ThingTypeUID(BINDING_ID, "station");
// the default update interval for statusses at the portal
public static final int DEFAULT_UPDATE_INTERVAL_MINUTES = 5;
// station properties
public static final String STATION_UUID = "stationUUID";
public static final String STATION_NAME = "stationName";
public static final String STATION_CAPACITY = "stationCapacity";
public static final String STATION_REPRESENTATION_PROPERTY = STATION_UUID;
public static final String STATION_LABEL_FORMAT = "Power Station %s";
// List of all Channel ids
public static final String CHANNEL_CURRENT_OUTPUT = "currentOutput";
public static final String CHANNEL_LASTUPDATE = "lastUpdate";
public static final String CHANNEL_TODAY_TOTAL = "todayTotal";
public static final String CHANNEL_MONTH_TOTAL = "monthTotal";
public static final String CHANNEL_OVERALL_TOTAL = "overallTotal";
public static final String CHANNEL_TODAY_INCOME = "todayIncome";
public static final String CHANNEL_TOTAL_INCOME = "totalIncome";
protected static final List<String> ALL_CHANNELS = Arrays.asList(CHANNEL_LASTUPDATE, CHANNEL_CURRENT_OUTPUT,
CHANNEL_TODAY_TOTAL, CHANNEL_MONTH_TOTAL, CHANNEL_OVERALL_TOTAL, CHANNEL_TODAY_INCOME,
CHANNEL_TOTAL_INCOME);
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SEMSPortalConfiguration} class contains fields mapping thing
* configuration parameters.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class SEMSPortalConfiguration {
/**
* We need username and password of the SEMS portal to access the solar plant
* data.
*
* In the first version, you need to provide the station ID as well. Later we
* can discover it from the SEMS portal.
*/
public String username = "";
public String password = "";
public int update = SEMSPortalBindingConstants.DEFAULT_UPDATE_INTERVAL_MINUTES;
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import static org.openhab.binding.semsportal.internal.SEMSPortalBindingConstants.*;
import java.util.Hashtable;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.semsportal.internal.discovery.StationDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link SEMSPortalHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.semsportal", service = ThingHandlerFactory.class)
public class SEMSPortalHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_STATION, THING_TYPE_PORTAL);
private HttpClientFactory httpClientFactory;
@Activate
public SEMSPortalHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_PORTAL.equals(thingTypeUID)) {
PortalHandler handler = new PortalHandler((Bridge) thing, httpClientFactory);
Hashtable<String, Object> dictionary = new Hashtable<>();
bundleContext.registerService(DiscoveryService.class.getName(), new StationDiscoveryService(handler),
dictionary);
return handler;
}
if (THING_TYPE_STATION.equals(thingTypeUID)) {
return new StationHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.semsportal.internal.dto.StationStatus;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Helper class to convert the POJOs of the SEMS portal response classes into openHAB State objects.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class StateHelper {
private StateHelper() {
// hide constructor, no initialisation possible
}
public static State getOperational(@Nullable StationStatus currentStatus) {
if (currentStatus == null) {
return UnDefType.UNDEF;
}
return OnOffType.from(currentStatus.isOperational());
}
public static State getLastUpdate(@Nullable StationStatus currentStatus) {
return currentStatus == null ? UnDefType.UNDEF : new DateTimeType(currentStatus.getLastUpdate());
}
public static State getCurrentOutput(@Nullable StationStatus status) {
if (status == null) {
return UnDefType.UNDEF;
}
if (status.getCurrentOutput() == null) {
return UnDefType.NULL;
}
return new QuantityType<>(status.getCurrentOutput(), Units.WATT);
}
public static State getDayTotal(@Nullable StationStatus status) {
if (status == null) {
return UnDefType.UNDEF;
}
if (status.getDayTotal() == null) {
return UnDefType.NULL;
}
return new QuantityType<>(status.getDayTotal(), Units.KILOWATT_HOUR);
}
public static State getMonthTotal(@Nullable StationStatus status) {
if (status == null) {
return UnDefType.UNDEF;
}
if (status.getMonthTotal() == null) {
return UnDefType.NULL;
}
return new QuantityType<>(status.getMonthTotal(), Units.KILOWATT_HOUR);
}
public static State getOverallTotal(@Nullable StationStatus status) {
if (status == null) {
return UnDefType.UNDEF;
}
if (status.getOverallTotal() == null) {
return UnDefType.NULL;
}
return new QuantityType<>(status.getOverallTotal(), Units.KILOWATT_HOUR);
}
public static State getDayIncome(@Nullable StationStatus status) {
if (status == null) {
return UnDefType.UNDEF;
}
if (status.getDayIncome() == null) {
return UnDefType.NULL;
}
return new DecimalType(status.getDayIncome());
}
public static State getTotalIncome(@Nullable StationStatus status) {
if (status == null) {
return UnDefType.UNDEF;
}
if (status.getTotalIncome() == null) {
return UnDefType.NULL;
}
return new DecimalType(status.getTotalIncome());
}
}

View File

@@ -0,0 +1,178 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import static org.openhab.binding.semsportal.internal.SEMSPortalBindingConstants.*;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.semsportal.internal.dto.StationStatus;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link StationHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class StationHandler extends BaseThingHandler {
private Logger logger = LoggerFactory.getLogger(StationHandler.class);
private static final long MAX_STATUS_AGE_MINUTES = 1;
private @Nullable StationStatus currentStatus;
private LocalDateTime lastUpdate = LocalDateTime.MIN;
public StationHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (isPortalOK()) {
if (command instanceof RefreshType) {
scheduler.execute(() -> {
ensureRecentStatus();
updateChannelState(channelUID);
});
}
}
}
private boolean isPortalOK() {
PortalHandler portal = getPortal();
return portal != null && portal.isLoggedIn();
}
private void updateChannelState(ChannelUID channelUID) {
if (!isPortalOK()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"Unable to update station info. Check Bridge status for details.");
return;
}
switch (channelUID.getId()) {
case CHANNEL_CURRENT_OUTPUT:
updateState(channelUID.getId(), StateHelper.getCurrentOutput(currentStatus));
break;
case CHANNEL_TODAY_TOTAL:
updateState(channelUID.getId(), StateHelper.getDayTotal(currentStatus));
break;
case CHANNEL_MONTH_TOTAL:
updateState(channelUID.getId(), StateHelper.getMonthTotal(currentStatus));
break;
case CHANNEL_OVERALL_TOTAL:
updateState(channelUID.getId(), StateHelper.getOverallTotal(currentStatus));
break;
case CHANNEL_TODAY_INCOME:
updateState(channelUID.getId(), StateHelper.getDayIncome(currentStatus));
break;
case CHANNEL_TOTAL_INCOME:
updateState(channelUID.getId(), StateHelper.getTotalIncome(currentStatus));
break;
case CHANNEL_LASTUPDATE:
updateState(channelUID.getId(), StateHelper.getLastUpdate(currentStatus));
break;
default:
logger.debug("No mapping found for channel {}", channelUID.getId());
}
}
private void ensureRecentStatus() {
if (lastUpdate.isBefore(LocalDateTime.now().minusMinutes(MAX_STATUS_AGE_MINUTES))) {
updateStation();
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> {
try {
scheduler.scheduleWithFixedDelay(() -> ensureRecentStatus(), 0, getUpdateInterval(), TimeUnit.MINUTES);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
"Unable to update station info. Check Bridge status for details.");
}
});
}
private long getUpdateInterval() {
PortalHandler portal = getPortal();
if (portal == null) {
return SEMSPortalBindingConstants.DEFAULT_UPDATE_INTERVAL_MINUTES;
}
return portal.getUpdateInterval();
}
private void updateStation() {
if (!isPortalOK()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"Unable to update station info. Check Bridge status for details.");
return;
}
PortalHandler portal = getPortal();
if (portal != null) {
try {
currentStatus = portal.getStationStatus(getStationUUID());
StationStatus localCurrentStatus = currentStatus;
if (localCurrentStatus != null && localCurrentStatus.isOperational()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, "Station not operational");
}
updateAllChannels();
} catch (CommunicationException commEx) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, commEx.getMessage());
} catch (ConfigurationException confEx) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, confEx.getMessage());
}
} else {
logger.debug("Unable to find portal for thing {}", getThing().getUID());
}
}
private String getStationUUID() {
String uuid = getThing().getProperties().get(STATION_UUID);
return uuid == null ? "" : uuid;
}
private void updateAllChannels() {
for (String channelName : ALL_CHANNELS) {
Channel channel = thing.getChannel(channelName);
if (channel != null) {
updateChannelState(channel.getUID());
}
}
}
private @Nullable PortalHandler getPortal() {
Bridge bridge = getBridge();
if (bridge != null && bridge.getHandler() != null && bridge.getHandler() instanceof PortalHandler) {
return (PortalHandler) bridge.getHandler();
}
return null;
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.discovery;
import static org.openhab.binding.semsportal.internal.SEMSPortalBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.semsportal.internal.PortalHandler;
import org.openhab.binding.semsportal.internal.dto.Station;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
/**
* The discovery service can discover the power stations that are registered to the portal that it belongs to. It will
* find unique power stations and add them as a discovery result;
*
* @author Iwan Bron - Initial contribution
*
*/
@NonNullByDefault
public class StationDiscoveryService extends AbstractDiscoveryService {
private static final int DISCOVERY_TIME = 10;
private PortalHandler portal;
private ThingUID bridgeUID;
public StationDiscoveryService(PortalHandler bridgeHandler) {
super(Set.of(THING_TYPE_STATION), DISCOVERY_TIME);
this.portal = bridgeHandler;
this.bridgeUID = bridgeHandler.getThing().getUID();
}
@Override
protected void startScan() {
for (Station station : portal.getAllStations()) {
DiscoveryResult discovery = DiscoveryResultBuilder.create(createThingUUID(station)).withBridge(bridgeUID)
.withProperties(buildProperties(station))
.withRepresentationProperty(STATION_REPRESENTATION_PROPERTY)
.withLabel(String.format(STATION_LABEL_FORMAT, station.getName())).withThingType(THING_TYPE_STATION)
.build();
thingDiscovered(discovery);
}
stopScan();
}
private ThingUID createThingUUID(Station station) {
return new ThingUID(THING_TYPE_STATION, station.getStationId(), bridgeUID.getId());
}
private @Nullable Map<String, Object> buildProperties(Station station) {
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_MODEL_ID, station.getType());
properties.put(Thing.PROPERTY_SERIAL_NUMBER, station.getSerialNumber());
properties.put(STATION_NAME, station.getName());
properties.put(STATION_CAPACITY, station.getCapacity());
properties.put(STATION_UUID, station.getStationId());
properties.put(STATION_CAPACITY, station.getCapacity());
return properties;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The base response contains the generic properties of each response from the portal. Depending on the request, the
* data component contains different information. The subclasses of the BaseResponse contain the mapping of the data
* with respect to their request context.
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class BaseResponse {
public static final String OK = "0";
public static final String NO_SESSION = "100001";
public static final String SESSION_EXPIRED = "100002";
public static final String INVALID = "100005";
public static final String EXCEPTION = "innerexception";
private @Nullable String code;
private @Nullable String msg;
public @Nullable String getCode() {
return code;
}
public @Nullable String getMsg() {
return msg;
}
public boolean isOk() {
return OK.equals(code);
}
public boolean isError() {
return EXCEPTION.equals(code);
}
public boolean isSessionInvalid() {
return NO_SESSION.equals(code) || SESSION_EXPIRED.equals(code);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
/**
* POJO containing details about the inverter. Only a very small subset of the available properties is mapped
*
* @author Iwan Bron - Initial contribution
*/
public class InverterDetails {
@SerializedName("last_refresh_time")
private Date lastUpdate;
public Date getLastUpdate() {
return lastUpdate;
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* POJO for mapping the SEMS portal data response /data/kpi
*
* @author Iwan Bron - Initial contribution
*
*/
public class KeyPerformanceIndicators {
@SerializedName("pac")
private Double currentOutput;
@SerializedName("month_generation")
private Double monthPower;
@SerializedName("total_power")
private Double totalPower;
@SerializedName("day_income")
private Double dayIncome;
@SerializedName("total_income")
private Double totalIncome;
@SerializedName("yield_rate")
private Double yieldRate;
private String currency;
public Double getCurrentOutput() {
return currentOutput;
}
public Double getMonthPower() {
return monthPower;
}
public Double getTotalPower() {
return totalPower;
}
public Double getDayIncome() {
return dayIncome;
}
public Double getTotalIncome() {
return totalIncome;
}
public Double getYieldRate() {
return yieldRate;
}
public String getCurrency() {
return currency;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The login request to the portal. Response can be deserialized in a {@link LoginResponse}
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class LoginRequest {
private String account;
private String pwd;
public LoginRequest(String account, String pwd) {
this.account = account;
this.pwd = pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public void setAccount(String account) {
this.account = account;
}
public String getPwd() {
return pwd;
}
public String getAccount() {
return account;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* Response to a {@link LoginRequest} to the portal.
*
* @author Iwan Bron - Initial contribution
*/
public class LoginResponse extends BaseResponse {
@SerializedName("data")
private SEMSToken token;
public SEMSToken getToken() {
return token;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
/**
* A token is returned in a successful {@Link LoginRequest} and is needed to authorize any subsequent requests.
*
* @author Iwan Bron - Initial contribution
*/
public class SEMSToken {
private String uid;
private long timestamp;
private String token;
private String client;
private String version;
private String language;
public SEMSToken(String version, String client, String language) {
this.version = version;
this.client = client;
this.language = language;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getClient() {
return client;
}
public void setClient(String client) {
this.client = client;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* POJO for mapping the portal data response to the {@link StatusRequest} and the {@Link StationListRequest}
*
* @author Iwan Bron - Initial contribution
*
*/
public class Station {
@SerializedName("powerstation_id")
private String stationId;
@SerializedName("stationname")
private String name;
@SerializedName("sn")
private String serialNumber;
private String type;
private Double capacity;
private int status;
@SerializedName("out_pac")
private Double currentPower;
@SerializedName("eday")
private Double dayTotal;
@SerializedName("emonth")
private Double monthTotal;
@SerializedName("etotal")
private Double overallTotal;
@SerializedName("d")
private InverterDetails details;
public String getStationId() {
return stationId;
}
public String getName() {
return name;
}
public String getSerialNumber() {
return serialNumber;
}
public String getType() {
return type;
}
public Double getCapacity() {
return capacity;
}
public int getStatus() {
return status;
}
public Double getCurrentPower() {
return currentPower;
}
public Double getDayTotal() {
return dayTotal;
}
public Double getMonthTotal() {
return monthTotal;
}
public Double getOverallTotal() {
return overallTotal;
}
public InverterDetails getDetails() {
return details;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Request to list all available power stations in an account. Answer can be deserialized in a
* {@link StationListResponse}
*
* @author Iwan Bron - Initial contribution
*
*/
@NonNullByDefault
public class StationListRequest {
// Properties are private but used by Gson to construct the request
@SerializedName("page_size")
private int pageSize = 5;
@SerializedName("page_index")
private int pageIndex = 1;
@SerializedName("order_by")
private String orderBy = "";
@SerializedName("powerstation_status")
private String powerstationStatus = "";
// @SerializedName("key")
// private String key = "";
@SerializedName("powerstation_id")
private String powerstationId = "";
@SerializedName("powerstation_type")
private String powerstationType = "";
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* POJO containing the response to the {@link StationListRequest}
*
* @author Iwan Bron - Initial contribution
*/
public class StationListResponse extends BaseResponse {
@SerializedName("data")
private List<Station> stations;
public List<Station> getStations() {
return stations;
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* Facade for easy access to the SEMS portal data response. Data is distributed over different parts of the response
* object
*
* @author Iwan Bron - Initial contribution
*/
public class StationStatus {
@SerializedName("kpi")
private KeyPerformanceIndicators keyPerformanceIndicators;
@SerializedName("inverter")
private List<Station> stations;
public Double getCurrentOutput() {
return keyPerformanceIndicators.getCurrentOutput();
}
public Double getDayTotal() {
return stations.isEmpty() ? null : stations.get(0).getDayTotal();
}
public Double getMonthTotal() {
return stations.isEmpty() ? null : stations.get(0).getMonthTotal();
}
public Double getOverallTotal() {
return stations.isEmpty() ? null : stations.get(0).getOverallTotal();
}
public Double getDayIncome() {
return keyPerformanceIndicators.getDayIncome();
}
public Double getTotalIncome() {
return keyPerformanceIndicators.getTotalIncome();
}
public boolean isOperational() {
return stations.isEmpty() ? false : stations.get(0).getStatus() == 1;
}
public ZonedDateTime getLastUpdate() {
if (stations.isEmpty()) {
return null;
}
return ZonedDateTime.ofInstant(stations.get(0).getDetails().getLastUpdate().toInstant(),
ZoneId.systemDefault());
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Request for the status of a Power Station. Answer can be deserialized in a {@link StatusResponse}
*
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class StatusRequest {
private String powerStationId;
public StatusRequest(String powerStationId) {
this.powerStationId = powerStationId;
}
public void setPowerStationId(String powerStationId) {
this.powerStationId = powerStationId;
}
public String getPowerStationId() {
return powerStationId;
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* POJO containing (a small subset of) the data received from the portal when issuing a {@link StationRequest)
*
* @author Iwan Bron - Initial contribution
*/
public class StatusResponse extends BaseResponse {
@SerializedName("data")
private StationStatus status;
public StationStatus getStatus() {
return status;
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="semsportal" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>SEMSPortal Binding</name>
<description>
This is the binding for SEMSPortal. The SEMS portal is where a GoodWE solar installation uploads it's
data. The SEMS portal has a lot of data, only a few of them are currently mapped to a channel.
You will need an account
at semsportal.com and have your solar installation registered on that account.
</description>
</binding:binding>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="semsportal"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="portal">
<label>SEMS Portal</label>
<description>The SEMS Portal is where the data about powerstations is collected online.
Configuration will only work if
you have used this account at least once in the portal itsself.</description>
<properties>
<property name="vendor">GoodWe</property>
</properties>
<representation-property>username</representation-property>
<config-description>
<parameter name="username" type="text" required="true">
<label>Username</label>
<description>Username (email address) of the account at the SEMS portal</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
<description>Password of the SEMS Portal</description>
</parameter>
<parameter name="interval" type="integer" min="1" max="60" unit="min">
<label>Interval</label>
<description>Number of minutes between updates. Minimum is 1 minute, maximum is 60. The default is 5 minutes.</description>
<default>5</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="station" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="portal"/>
</supported-bridge-type-refs>
<label>Power Station</label>
<description>A Power Station is the GoodWe converter that is connected through the internet with the SEMSPortal.</description>
<channels>
<channel id="lastUpdate" typeId="lastUpdate"/>
<channel id="currentOutput" typeId="currentOutput"/>
<channel id="todayTotal" typeId="todayTotal"/>
<channel id="monthTotal" typeId="monthTotal"/>
<channel id="overallTotal" typeId="overallTotal"/>
<channel id="todayIncome" typeId="todayIncome"/>
<channel id="totalIncome" typeId="totalIncome"/>
</channels>
</thing-type>
<channel-type id="lastUpdate">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>Timestamp that the last information was received from the station. This is not the same as the last time
that was checked: the station goes offline at night.</description>
</channel-type>
<channel-type id="currentOutput">
<item-type>Number:Power</item-type>
<label>Current Output</label>
<description>Current output in Watts</description>
</channel-type>
<channel-type id="todayTotal">
<item-type>Number:Energy</item-type>
<label>Todays Total Output</label>
<description>Todays total output in kWh</description>
</channel-type>
<channel-type id="monthTotal">
<item-type>Number:Energy</item-type>
<label>Current Month Total Output</label>
<description>The total output of this month in kWh</description>
</channel-type>
<channel-type id="overallTotal">
<item-type>Number:Energy</item-type>
<label>Overall Total Output</label>
<description>The total output from the start of the installation in kWh</description>
</channel-type>
<channel-type id="todayIncome">
<item-type>Number</item-type>
<label>Todays Income</label>
<description>Todays income. Only reports if you have set the tariffs in the SEMS portal. Unit is the currency that is
set in these tariffs.</description>
</channel-type>
<channel-type id="totalIncome">
<item-type>Number</item-type>
<label>Total Income</label>
<description>Total income since installation. Only reports if you have set the tariffs in the SEMS portal. Unit is the
currency that is set in these tariffs.</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.semsportal.internal;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.semsportal.internal.dto.BaseResponse;
import org.openhab.binding.semsportal.internal.dto.LoginResponse;
import org.openhab.binding.semsportal.internal.dto.StationListResponse;
import org.openhab.binding.semsportal.internal.dto.StatusResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* @author Iwan Bron - Initial contribution
*/
@NonNullByDefault
public class SEMSJsonParserTest {
@Test
public void testParseSuccessStatusResult() throws Exception {
String json = Files.readString(Paths.get("src/test/resources/success_status.json"));
StatusResponse response = getGson().fromJson(json, StatusResponse.class);
assertNotNull(response, "Expected deserialized StatusResponse");
if (response != null) {// response cannot be null, was asserted before, but code check produces a warning
assertTrue(response.isOk(), "Successresponse should be OK");
assertNotNull(response.getStatus(), "Expected deserialized StatusResponse.status");
assertEquals(381.0, response.getStatus().getCurrentOutput(), "Current Output parsed correctly");
assertEquals(0.11, response.getStatus().getDayIncome(), "Day income parsed correctly");
assertEquals(0.5, response.getStatus().getDayTotal(), "Day total parsed correctly");
assertEquals(ZonedDateTime.of(2021, 2, 6, 11, 22, 48, 0, ZoneId.systemDefault()),
response.getStatus().getLastUpdate(), "Last update parsed correctly");
assertEquals(17.2, response.getStatus().getMonthTotal(), "Month total parsed correctly");
assertEquals(7379.0, response.getStatus().getOverallTotal(), "Overall total parsed correctly");
assertEquals(823.38, response.getStatus().getTotalIncome(), "Total income parsed correctly");
}
}
@Test
public void testParseErrorStatusResult() throws Exception {
String json = Files.readString(Paths.get("src/test/resources/error_status.json"));
BaseResponse response = getGson().fromJson(json, BaseResponse.class);
assertNotNull(response, "Expected deserialized StatusResponse");
if (response != null) {// response cannot be null, was asserted before, but code check produces a warning
assertEquals(response.getCode(), BaseResponse.EXCEPTION, "Error response shoud have error code");
assertTrue(response.isError(), "Error response should have isError = true");
}
}
@Test
public void testParseSuccessLoginResult() throws Exception {
String json = Files.readString(Paths.get("src/test/resources/success_login.json"));
LoginResponse response = getGson().fromJson(json, LoginResponse.class);
assertNotNull(response, "Expected deserialized LoginResponse");
if (response != null) {// response cannot be null, was asserted before, but code check produces a warning
assertTrue(response.isOk(), "Success response should result in OK");
assertNotNull(response.getToken(), "Success response should result in token");
}
}
@Test
public void testParseErrorLoginResult() throws Exception {
String json = Files.readString(Paths.get("src/test/resources/error_login.json"));
LoginResponse response = getGson().fromJson(json, LoginResponse.class);
assertNotNull(response, "Expected deserialized LoginResponse");
if (response != null) {// response cannot be null, was asserted before, but code check produces a warning
assertFalse(response.isOk(), "Error response should not result in OK");
assertNull(response.getToken(), "Error response should have null token");
}
}
@Test
public void testParseSuccessListResult() throws Exception {
String json = Files.readString(Paths.get("src/test/resources/success_list.json"));
StationListResponse response = getGson().fromJson(json, StationListResponse.class);
assertNotNull(response, "Expected deserialized StationListResponse");
if (response != null) {// response cannot be null, was asserted before, but code check produces a warning
assertTrue(response.isOk(), "Success response should result in OK");
assertNotNull(response.getStations(), "List response should have station list");
assertEquals(1, response.getStations().size(), "List response should have station list");
}
}
private Gson getGson() {
return new GsonBuilder().setDateFormat(SEMSPortalBindingConstants.DATE_FORMAT).create();
}
}

View File

@@ -0,0 +1,13 @@
{
"hasError": false,
"code": 100005,
"msg": "Email or password error.",
"data": null,
"components": {
"para": null,
"langVer": 97,
"timeSpan": 0,
"api": "http://www.semsportal.com:82/api/v2/Common/CrossLogin",
"msgSocketAdr": "https://eu-xxzx.semsportal.com"
}
}

View File

@@ -0,0 +1,17 @@
{
"language": "en",
"function": [
"ADD"
],
"hasError": true,
"msg": "未将对象引用设置到对象的实例。",
"code": "innerexception",
"data": "",
"components": {
"para": "{\"model\":{\"PowerStationId\":\"ERRORCODE\"}}",
"langVer": 97,
"timeSpan": 8,
"api": "http://www.semsportal.com:82/api/v2/PowerStation/GetMonitorDetailByPowerstationId",
"msgSocketAdr": null
}
}

View File

@@ -0,0 +1,73 @@
{
"hasError": false,
"code": 0,
"msg": "Success",
"data": [
{
"powerstation_id": "000000-0000000-0000000-00000",
"stationname": "place",
"first_letter": "",
"adcode": "11111111111111",
"location": "",
"status": -1,
"pac": 0.0,
"capacity": 4.5,
"eday": 13.1,
"emonth": 195.0,
"eday_income": 604.318,
"etotal": 2746.9,
"powerstation_type": "residential",
"pre_org_id": null,
"org_id": null,
"longitude": "16",
"latitude": "23",
"pac_kw": 2746.9,
"to_hour": 2.911111111111111,
"weather": {
"HeWeather6": [
{
"basic": {
"cid": "XXXXXXXX",
"location": "Location",
"parent_city": "City",
"admin_area": "Area",
"cnty": "Country",
"lat": "16",
"lon": "23",
"tz": "+1.00"
},
"update": {
"loc": "2019-08-12 23:57",
"utc": "2019-08-12 22:57"
},
"status": "ok",
"now": {
"cloud": "100",
"cond_code": "300",
"cond_txt": "Shower Rain",
"fl": "14",
"hum": "100",
"pcpn": "0.3",
"pres": "1014",
"tmp": "14",
"vis": "16",
"wind_deg": "90",
"wind_dir": "E",
"wind_sc": "1",
"wind_spd": "4"
}
}
]
},
"currency": "EUR",
"yield_rate": 0.22,
"is_stored": false
}
],
"components": {
"para": null,
"langVer": 40,
"timeSpan": 0,
"api": "http://eu.semsportal.com:82/api/PowerStationMonitor/QueryPowerStationMonitorForApp"
}
}

View File

@@ -0,0 +1,21 @@
{
"hasError": false,
"code": 0,
"msg": "Success",
"data": {
"uid": "0000000-0000-0000-00000000",
"timestamp": 1612644477008,
"token": "12345678",
"client": "ios",
"version": "v2.1.0",
"language": "en"
},
"components": {
"para": null,
"langVer": 97,
"timeSpan": 0,
"api": "http://eu.semsportal.com:82/api/Auth/GetTokenV2",
"msgSocketAdr": "https://eu-xxzx.semsportal.com"
},
"api": "https://eu.semsportal.com/api/"
}

View File

@@ -0,0 +1,941 @@
{
"language": "en",
"function": [
"ADD",
"VIEW",
"EDIT",
"DELETE",
"INVERTER_A",
"INVERTER_E",
"INVERTER_D"
],
"hasError": false,
"msg": "success",
"code": "0",
"data": {
"info": {
"powerstation_id": "RANDOM_UUID",
"time": "02/06/2021 11:24:41",
"date_format": "MM.dd.yyyy",
"date_format_ym": "MM.yyyy",
"stationname": "Home",
"address": "Somewhere",
"owner_name": null,
"owner_phone": null,
"owner_email": "some@mailaddress.com",
"battery_capacity": 0.0,
"turnon_time": "04/06/2020 18:19:40",
"create_time": "04/06/2020 18:18:32",
"capacity": 7.8,
"longitude": 16,
"latitude": 23,
"powerstation_type": "Residential",
"status": 1,
"is_stored": false,
"is_powerflow": false,
"charts_type": 4,
"has_pv": true,
"has_statistics_charts": false,
"only_bps": false,
"only_bpu": false,
"time_span": -1.0,
"pr_value": ""
},
"kpi": {
"month_generation": 17.7,
"pac": 381.0,
"power": 0.5,
"total_power": 7379.0,
"day_income": 0.11,
"total_income": 823.38,
"yield_rate": 0.12,
"currency": "EUR"
},
"images": [],
"weather": {
"HeWeather6": [
{
"daily_forecast": [
{
"cond_code_d": "407",
"cond_code_n": "499",
"cond_txt_d": "Snow Flurry",
"cond_txt_n": "Snow",
"date": "2021-02-06",
"time": "2021-02-06 00:00:00",
"hum": "79",
"pcpn": "0.2",
"pop": "49",
"pres": "1011",
"tmp_max": "0",
"tmp_min": "-5",
"uv_index": "0",
"vis": "6",
"wind_deg": "84",
"wind_dir": "E",
"wind_sc": "4-5",
"wind_spd": "34"
},
{
"cond_code_d": "499",
"cond_code_n": "499",
"cond_txt_d": "Snow",
"cond_txt_n": "Snow",
"date": "2021-02-07",
"time": "2021-02-07 00:00:00",
"hum": "93",
"pcpn": "7.2",
"pop": "82",
"pres": "1006",
"tmp_max": "-3",
"tmp_min": "-6",
"uv_index": "0",
"vis": "1",
"wind_deg": "70",
"wind_dir": "NE",
"wind_sc": "6-7",
"wind_spd": "46"
},
{
"cond_code_d": "499",
"cond_code_n": "101",
"cond_txt_d": "Snow",
"cond_txt_n": "Cloudy",
"date": "2021-02-08",
"time": "2021-02-08 00:00:00",
"hum": "94",
"pcpn": "1.1",
"pop": "55",
"pres": "1005",
"tmp_max": "-3",
"tmp_min": "-7",
"uv_index": "0",
"vis": "1",
"wind_deg": "84",
"wind_dir": "E",
"wind_sc": "3-4",
"wind_spd": "20"
},
{
"cond_code_d": "901",
"cond_code_n": "901",
"cond_txt_d": "Cold",
"cond_txt_n": "Cold",
"date": "2021-02-09",
"time": "2021-02-09 00:00:00",
"hum": "92",
"pcpn": "0.0",
"pop": "25",
"pres": "1009",
"tmp_max": "-4",
"tmp_min": "-9",
"uv_index": "1",
"vis": "24",
"wind_deg": "85",
"wind_dir": "E",
"wind_sc": "3-4",
"wind_spd": "18"
},
{
"cond_code_d": "901",
"cond_code_n": "103",
"cond_txt_d": "Cold",
"cond_txt_n": "Partly Cloudy",
"date": "2021-02-10",
"time": "2021-02-10 00:00:00",
"hum": "90",
"pcpn": "0.0",
"pop": "12",
"pres": "1019",
"tmp_max": "-3",
"tmp_min": "-7",
"uv_index": "1",
"vis": "25",
"wind_deg": "67",
"wind_dir": "NE",
"wind_sc": "3-4",
"wind_spd": "14"
},
{
"cond_code_d": "103",
"cond_code_n": "101",
"cond_txt_d": "Partly Cloudy",
"cond_txt_n": "Cloudy",
"date": "2021-02-11",
"time": "2021-02-11 00:00:00",
"hum": "93",
"pcpn": "0.0",
"pop": "8",
"pres": "1018",
"tmp_max": "-1",
"tmp_min": "-7",
"uv_index": "2",
"vis": "6",
"wind_deg": "187",
"wind_dir": "S",
"wind_sc": "1-2",
"wind_spd": "11"
},
{
"cond_code_d": "999",
"cond_code_n": "999",
"cond_txt_d": "-",
"cond_txt_n": "-",
"date": "2021-02-12",
"time": "2021-02-12 00:00:00",
"hum": "-",
"pcpn": "-",
"pop": "-",
"pres": "-",
"tmp_max": "-",
"tmp_min": "-",
"uv_index": "-",
"vis": "-",
"wind_deg": "-",
"wind_dir": "--",
"wind_sc": "--",
"wind_spd": "-"
}
],
"basic": {
"cid": "NL2755251",
"location": "SomeCity",
"cnty": "SomeCountry",
"lat": "13",
"lon": "26",
"tz": "+5.00"
},
"update": {
"loc": "2021-02-05 23:56:00",
"utc": "2021-02-05 22:56:00"
},
"status": "ok"
}
]
},
"inverter": [
{
"sn": "56000222200W0000",
"dict": {
"left": [
{
"isHT": false,
"key": "dmDeviceType",
"value": "GW6000-DT",
"unit": "",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "serialNum",
"value": "5600000000000000",
"unit": "",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "laCheckcode",
"value": "026848",
"unit": "",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "capacity",
"value": "6",
"unit": "kW",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "laConnected",
"value": "04.06.2020 18:19:40",
"unit": "",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "InverterPowerOfPlantMonitor",
"value": "0.381",
"unit": "kW",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "outputV",
"value": "233.1/233.5/228.7",
"unit": "V",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "acCurrent",
"value": "0.5/0.4/0.5",
"unit": "A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "acFrequency",
"value": "50.00/50.00/50.00",
"unit": "Hz",
"isFaultMsg": 0,
"faultMsgCode": 0
}
],
"right": [
{
"isHT": false,
"key": "innerTemp",
"value": "32.6",
"unit": "℃",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "dcVandC1",
"value": "720.4/0.4",
"unit": "V/A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "dcVandC2",
"value": "0.0/0.0",
"unit": "V/A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "dcVandC3",
"value": "--",
"unit": "V/A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "dcVandC4",
"value": "--",
"unit": "V/A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "strCurrent1",
"value": "--",
"unit": "A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "strCurrent2",
"value": "--",
"unit": "A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "strCurrent3",
"value": "--",
"unit": "A",
"isFaultMsg": 0,
"faultMsgCode": 0
},
{
"isHT": false,
"key": "strCurrent4",
"value": "--",
"unit": "A",
"isFaultMsg": 0,
"faultMsgCode": 0
}
]
},
"is_stored": false,
"name": "home",
"in_pac": 6554.3,
"out_pac": 381.0,
"eday": 0.5,
"emonth": 17.2,
"etotal": 7379.0,
"status": 1,
"turnon_time": "04/06/2020 18:19:40",
"releation_id": "5600000000000000",
"type": "GW6000-DT",
"capacity": 6.0,
"d": {
"pw_id": "5600000000000000",
"capacity": "6kW",
"model": "GW6000-DT",
"output_power": "381W",
"output_current": "0.5A",
"grid_voltage": "233.1V",
"backup_output": "4294967.295V/0W",
"soc": "400%",
"soh": "0%",
"last_refresh_time": "02.06.2021 11:22:48",
"work_mode": "Wait Mode",
"dc_input1": "720.4V/0.4A",
"dc_input2": "0V/0A",
"battery": "65535V/6553.5A/429483622W",
"bms_status": "DischargingOfBattery",
"warning": "Over Temperature/Under Temperature/Cell Voltage Differences/Over Total Voltage/Discharge Over Current/Charge Over Current/Under SOC/Under Total Voltage/Communication Fail/Output Short/SOC Too High/BMS Module Fault/BMS System Fault/BMS Internal Fault/TBD/TBD",
"charge_current_limit": "1A",
"discharge_current_limit": "0A",
"firmware_version": 181809.0,
"creationDate": "02/06/2021 18:22:48",
"eDay": 0.5,
"eTotal": 7379.0,
"pac": 381.0,
"hTotal": 5132.0,
"vpv1": 720.4,
"vpv2": 0.0,
"vpv3": 6553.5,
"vpv4": 6553.5,
"ipv1": 0.4,
"ipv2": 0.0,
"ipv3": 6553.5,
"ipv4": 6553.5,
"vac1": 233.1,
"vac2": 233.5,
"vac3": 228.7,
"iac1": 0.5,
"iac2": 0.4,
"iac3": 0.5,
"fac1": 50.0,
"fac2": 50.0,
"fac3": 50.0,
"istr1": 0.0,
"istr2": 0.0,
"istr3": 0.0,
"istr4": 0.0,
"istr5": 0.0,
"istr6": 0.0,
"istr7": 0.0,
"istr8": 0.0,
"istr9": 0.0,
"istr10": 0.0,
"istr11": 0.0,
"istr12": 0.0,
"istr13": 0.0,
"istr14": 0.0,
"istr15": 0.0,
"istr16": 0.0
},
"it_change_flag": false,
"tempperature": 32.6,
"check_code": "026848",
"next": null,
"prev": null,
"next_device": {
"sn": null,
"isStored": false
},
"prev_device": {
"sn": null,
"isStored": false
},
"invert_full": {
"sn": "5600000000000000",
"powerstation_id": "5600000000000000",
"name": "home",
"model_type": "GW6000-DT",
"change_type": 0,
"change_time": 0,
"capacity": 6.0,
"eday": 0.5,
"iday": 0.11,
"etotal": 7379.0,
"itotal": 1623.38,
"hour_total": 5132.0,
"status": 1,
"turnon_time": 1586168380200,
"pac": 381.0,
"tempperature": 32.6,
"vpv1": 720.4,
"vpv2": 0.0,
"vpv3": 6553.5,
"vpv4": 6553.5,
"ipv1": 0.4,
"ipv2": 0.0,
"ipv3": 6553.5,
"ipv4": 6553.5,
"vac1": 233.1,
"vac2": 233.5,
"vac3": 228.7,
"iac1": 0.5,
"iac2": 0.4,
"iac3": 0.5,
"fac1": 50.0,
"fac2": 50.0,
"fac3": 50.0,
"istr1": 0.0,
"istr2": 0.0,
"istr3": 0.0,
"istr4": 0.0,
"istr5": 0.0,
"istr6": 0.0,
"istr7": 0.0,
"istr8": 0.0,
"istr9": 0.0,
"istr10": 0.0,
"istr11": 0.0,
"istr12": 0.0,
"istr13": 0.0,
"istr14": 0.0,
"istr15": 0.0,
"istr16": 0.0,
"last_time": 1612606968847,
"vbattery1": 65535.0,
"ibattery1": 6553.5,
"pmeter": 381.0,
"soc": 400.0,
"soh": -0.100000000000364,
"bms_discharge_i_max": null,
"bms_charge_i_max": 1.0,
"bms_warning": 0,
"bms_alarm": 65535,
"battary_work_mode": 2,
"workmode": 1,
"vload": 4294967.295,
"iload": 4294901.76,
"firmwareversion": 1818.0,
"pbackup": 0.0,
"seller": 0.0,
"buy": 0.0,
"yesterdaybuytotal": null,
"yesterdaysellertotal": null,
"yesterdayct2sellertotal": null,
"yesterdayetotal": null,
"yesterdayetotalload": null,
"thismonthetotle": 17.2,
"lastmonthetotle": 7361.3,
"ram": 9.0,
"outputpower": 381.0,
"fault_messge": 0,
"isbuettey": false,
"isbuetteybps": false,
"isbuetteybpu": false,
"isESUOREMU": false,
"backUpPload_S": 0.0,
"backUpVload_S": 0.0,
"backUpIload_S": 0.0,
"backUpPload_T": 0.0,
"backUpVload_T": 0.0,
"backUpIload_T": 0.0,
"eTotalBuy": null,
"eDayBuy": null,
"eBatteryCharge": null,
"eChargeDay": null,
"eBatteryDischarge": null,
"eDischargeDay": null,
"battStrings": 6553.5,
"meterConnectStatus": null,
"mtActivepowerR": 0.0,
"mtActivepowerS": 0.0,
"mtActivepowerT": 0.0,
"ezPro_connect_status": null,
"dataloggersn": "",
"equipment_name": null,
"hasmeter": false,
"meter_type": null,
"pre_hour_lasttotal": null,
"pre_hour_time": null,
"current_hour_pv": 0.0,
"extend_properties": null,
"eP_connect_status_happen": null,
"eP_connect_status_recover": null
},
"time": "02/06/2021 11:24:41",
"battery": "65535V/6553.5A/429483622W",
"firmware_version": 181809.0,
"warning_bms": "Over Temperature/Under Temperature/Cell Voltage Differences/Over Total Voltage/Discharge Over Current/Charge Over Current/Under SOC/Under Total Voltage/Communication Fail/Output Short/SOC Too High/BMS Module Fault/BMS System Fault/BMS Internal Fault/TBD/TBD",
"soh": "0%",
"discharge_current_limit_bms": "0A",
"charge_current_limit_bms": "1A",
"soc": "400%",
"pv_input_2": "0V/0A",
"pv_input_1": "720.4V/0.4A",
"back_up_output": "4294967.295V/0W",
"output_voltage": "233.1V",
"backup_voltage": "4294967.295V",
"output_current": "0.5A",
"output_power": "381W",
"total_generation": "7379kWh",
"daily_generation": "0.5kWh",
"battery_charging": "65535V/6553.5A/429483622W",
"last_refresh_time": "02/06/2021 11:22:48",
"bms_status": "DischargingOfBattery",
"pw_id": "0000000-0000-0000-00000000",
"fault_message": "",
"battery_power": 429483622.5,
"point_index": "3",
"points": [
{
"target_index": 1,
"target_name": "Vpv1",
"display": "Vpv1(V)",
"unit": "V",
"target_key": "Vpv1",
"text_cn": "直流电压1",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 2,
"target_name": "Vpv2",
"display": "Vpv2(V)",
"unit": "V",
"target_key": "Vpv2",
"text_cn": "直流电压2",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 5,
"target_name": "Ipv1",
"display": "Ipv1(A)",
"unit": "A",
"target_key": "Ipv1",
"text_cn": "直流电流1",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 6,
"target_name": "Ipv2",
"display": "Ipv2(A)",
"unit": "A",
"target_key": "Ipv2",
"text_cn": "直流电流2",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 9,
"target_name": "Vac1",
"display": "Vac1(V)",
"unit": "V",
"target_key": "Vac1",
"text_cn": "交流电压1",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 10,
"target_name": "Vac2",
"display": "Vac2(V)",
"unit": "V",
"target_key": "Vac2",
"text_cn": "交流电压2",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 11,
"target_name": "Vac3",
"display": "Vac3(V)",
"unit": "V",
"target_key": "Vac3",
"text_cn": "交流电压3",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 12,
"target_name": "Iac1",
"display": "Iac1(A)",
"unit": "A",
"target_key": "Iac1",
"text_cn": "交流电流1",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 13,
"target_name": "Iac2",
"display": "Iac2(A)",
"unit": "A",
"target_key": "Iac2",
"text_cn": "交流电流2",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 14,
"target_name": "Iac3",
"display": "Iac3(A)",
"unit": "A",
"target_key": "Iac3",
"text_cn": "交流电流3",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 15,
"target_name": "Fac1",
"display": "Fac1(Hz)",
"unit": "Hz",
"target_key": "Fac1",
"text_cn": "频率1",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 16,
"target_name": "Fac2",
"display": "Fac2(Hz)",
"unit": "Hz",
"target_key": "Fac2",
"text_cn": "频率2",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 17,
"target_name": "Fac3",
"display": "Fac3(Hz)",
"unit": "Hz",
"target_key": "Fac3",
"text_cn": "频率3",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 18,
"target_name": "Pac",
"display": "Pac(W)",
"unit": "W",
"target_key": "Pac",
"text_cn": "功率",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 19,
"target_name": "WorkMode",
"display": "WorkMode()",
"unit": "",
"target_key": "WorkMode",
"text_cn": "工作模式",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 20,
"target_name": "Temperature",
"display": "Temperature(℃)",
"unit": "℃",
"target_key": "Tempperature",
"text_cn": "温度",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 21,
"target_name": "Daily Generation",
"display": "Daily Generation(kWh)",
"unit": "kWh",
"target_key": "EDay",
"text_cn": "日发电量",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 22,
"target_name": "Total Generation",
"display": "Total Generation(kWh)",
"unit": "kWh",
"target_key": "ETotal",
"text_cn": "总发电量",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 23,
"target_name": "HTotal",
"display": "HTotal(h)",
"unit": "h",
"target_key": "HTotal",
"text_cn": "工作时长",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
},
{
"target_index": 36,
"target_name": "RSSI",
"display": "RSSI(%)",
"unit": "%",
"target_key": "Reserved5",
"text_cn": "GPRS信号强度",
"target_sn_six": null,
"target_sn_seven": null,
"target_type": null,
"storage_name": null
}
],
"backup_pload_s": 0.0,
"backup_vload_s": 0.0,
"backup_iload_s": 0.0,
"backup_pload_t": 0.0,
"backup_vload_t": 0.0,
"backup_iload_t": 0.0,
"etotal_buy": null,
"eday_buy": null,
"ebattery_charge": null,
"echarge_day": null,
"ebattery_discharge": null,
"edischarge_day": null,
"batt_strings": 6553.5,
"meter_connect_status": null,
"mtactivepower_r": 0.0,
"mtactivepower_s": 0.0,
"mtactivepower_t": 0.0,
"has_tigo": false,
"canStartIV": false
}
],
"hjgx": {
"co2": 7.3568630000000006,
"tree": 403.26234999999997,
"coal": 2.981116
},
"pre_powerstation_id": null,
"nex_powerstation_id": null,
"homKit": {
"homeKitLimit": false,
"sn": null
},
"isTigo": false,
"smuggleInfo": {
"isAllSmuggle": false,
"isSmuggle": false,
"descriptionText": null,
"sns": null
},
"hasPowerflow": false,
"powerflow": null,
"hasEnergeStatisticsCharts": false,
"energeStatisticsCharts": {
"contributingRate": 1.0,
"selfUseRate": 1.0,
"sum": 0.5,
"buy": 0.0,
"buyPercent": 0.0,
"sell": 0.0,
"sellPercent": 0.0,
"selfUseOfPv": 0.5,
"consumptionOfLoad": 0.5,
"chartsType": 4,
"hasPv": true,
"hasCharge": false,
"charge": 0.0,
"disCharge": 0.0
},
"energeStatisticsTotals": {
"contributingRate": 1.0,
"selfUseRate": 1.0,
"sum": 157.2,
"buy": 0.0,
"buyPercent": 0.0,
"sell": 0.0,
"sellPercent": 0.0,
"selfUseOfPv": 157.2,
"consumptionOfLoad": 157.2,
"chartsType": 4,
"hasPv": true,
"hasCharge": false,
"charge": 0.0,
"disCharge": 0.0
},
"soc": {
"power": 0,
"status": -1
},
"environmental": [],
"equipment": [
{
"type": "5",
"title": "home",
"status": 1,
"statusText": null,
"capacity": null,
"actionThreshold": null,
"subordinateEquipment": "",
"powerGeneration": "Power0.381(kW)",
"eday": "Today Generation 0.5(kWh)",
"brand": "",
"isStored": false,
"soc": "SOC400%",
"isChange": false,
"relationId": "0000000-0000-0000-00000000",
"sn": "5600000000000000",
"has_tigo": false,
"is_sec": false,
"is_secs": false,
"targetPF": null,
"exportPowerlimit": null
}
]
},
"components": {
"para": "{\"model\":{\"PowerStationId\":\"0000000-0000-0000-00000000\"}}",
"langVer": 97,
"timeSpan": 177,
"api": "http://www.semsportal.com:82/api/v2/PowerStation/GetMonitorDetailByPowerstationId",
"msgSocketAdr": null
}
}