[generacmobilelink] Major rewrite of the Generac MobileLink Binding (#14638)

* [generacmobilelink] Major rewrite of the Generac MobileLink Binding

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham 2023-04-09 02:48:12 -07:00 committed by GitHub
parent 09c394b928
commit 41701f518e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 900 additions and 419 deletions

View File

@ -36,22 +36,25 @@ The MobileLink account bridge must be added manually. Once added, generator thin
All channels are read-only.
| channel | type | description |
|-------------------------|----------------------|-------------------------------------------|
| connected | Switch | Connected status |
| greenLight | Switch | Green light state (typically auto mode) |
| yellowLight | Switch | Yellow light state |
| redLight | Switch | Red light state (typically off mode) |
| blueLight | Switch | Blue light state (typically running mode) |
| statusDate | DateTime | Status date (start of day) |
| status | String | General status |
| currentAlarmDescription | String | Current alarm description |
| runHours | Number:Time | Number of run hours |
| exerciseHours | Number:Time | Number of exercise hours |
| fuelType | Number | Fuel type |
| fuelLevel | Number:Dimensionless | Fuel level |
| batteryVoltage | String | Battery voltage status |
| serviceStatus | Switch | Service status |
| Channel ID | Item Type | Description |
|----------------------|-----------------------------|-----------------------------------|
| heroImageUrl | String | Hero Image URL |
| statusLabel | String | Status Label |
| statusText | String | Status Text |
| activationDate | DateTime | Activation Date |
| deviceSsid | String | Device SSID |
| status | Number | Status |
| isConnected | Switch | Is Connected |
| isConnecting | Switch | Is Connecting |
| showWarning | Switch | Show Warning |
| hasMaintenanceAlert | Switch | Has Maintenance Alert |
| lastSeen | DateTime | Last Seen |
| connectionTime | DateTime | Connection Time |
| runHours | Number:Time | Number of Hours Run |
| batteryVoltage | Number:ElectricPotential | Battery Voltage |
| hoursOfProtection | Number:Time | Number of Hours of Protection |
| signalStrength | Number:Dimensionless | Signal Strength |
## Full Example
@ -66,27 +69,41 @@ Bridge generacmobilelink:account:main "MobileLink Account" [ userName="foo@bar.c
### Items
```java
Switch GeneratorConnected "Connected [%s]" {channel="generacmobilelink:generator:main:123456:connected"}
Switch GeneratorGreenLight "Green Light [%s]" {channel="generacmobilelink:generator:main:123456:greenLight"}
Switch GeneratorYellowLight "Yellow Light [%s]" {channel="generacmobilelink:generator:main:123456:yellowLight"}
Switch GeneratorBlueLight "Blue Light [%s]" {channel="generacmobilelink:generator:main:123456:blueLight"}
Switch GeneratorRedLight "Red Light [%s]" {channel="generacmobilelink:generator:main:123456:redLight"}
String GeneratorStatus "Status [%s]" {channel="generacmobilelink:generator:main:123456:status"}
String GeneratorAlarm "Alarm [%s]" {channel="generacmobilelink:generator:main:123456:currentAlarmDescription"}
String GeneratorHeroImageUrl "Hero Image URL [%s]" { channel="generacmobilelink:generator:main:123456:heroImageUrl" }
String GeneratorStatusLabel "Status Label [%s]" { channel="generacmobilelink:generator:main:123456:statusLabel" }
String GeneratorStatusText "Status Text [%s]" { channel="generacmobilelink:generator:main:123456:statusText" }
DateTime GeneratorActivationDate "Activation Date [%s]" { channel="generacmobilelink:generator:main:123456:activationDate" }
String GeneratorDeviceSsid "Device SSID [%s]" { channel="generacmobilelink:generator:main:123456:deviceSsid" }
Number GeneratorStatus "Status [%d]" { channel="generacmobilelink:generator:main:123456:status" }
Switch GeneratorIsConnected "Is Connected [%s]" { channel="generacmobilelink:generator:main:123456:isConnected" }
Switch GeneratorIsConnecting "Is Connecting [%s]" { channel="generacmobilelink:generator:main:123456:isConnecting" }
Switch GeneratorShowWarning "Show Warning [%s]" { channel="generacmobilelink:generator:main:123456:showWarning" }
Switch GeneratorHasMaintenanceAlert "Has Maintenance Alert [%s]" { channel="generacmobilelink:generator:main:123456:hasMaintenanceAlert" }
DateTime GeneratorLastSeen "Last Seen [%s]" { channel="generacmobilelink:generator:main:123456:lastSeen" }
DateTime GeneratorConnectionTime "Connection Time [%s]" { channel="generacmobilelink:generator:main:123456:connectionTime" }
Number:Time GeneratorRunHours "Number of Hours Run [%d]" { channel="generacmobilelink:generator:main:123456:runHours" }
Number:ElectricPotential GeneratorBatteryVoltage "Battery Voltage [%d]v" { channel="generacmobilelink:generator:main:123456:batteryVoltage" }
Number:Time GeneratorHoursOfProtection "Number of Hours of Protection [%d]" { channel="generacmobilelink:generator:main:123456:hoursOfProtection" }
Number:Dimensionless GeneratorSignalStrength "Signal Strength [%d]" { channel="generacmobilelink:generator:main:123456:signalStrength" }
```
### Sitemap
```perl
sitemap MobileLink label="Demo Sitemap" {
Frame label="Generator" {
Switch item=GeneratorConnected
Switch item=GeneratorGreenLight
Switch item=GeneratorYellowLight
Switch item=GeneratorBlueLight
Switch item=GeneratorRedLight
Text item=GeneratorStatus
Text item=GeneratorAlarm
}
sitemap generacmobilelink label="Generac MobileLink"
{
Frame label="Generator Status" {
Text item=GeneratorStatus
Text item=GeneratorStatusLabel
Text item=GeneratorStatusText
}
Frame label="Generator Properties" {
Text item=GeneratorRunHours
Text item=GeneratorHoursOfProtection
Text item=GeneratorBatteryVoltage
Text item=GeneratorSignalStrength
}
}
```

View File

@ -14,4 +14,12 @@
<name>openHAB Add-ons :: Bundles :: GeneracMobileLink Binding</name>
<dependencies>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -4,6 +4,7 @@
<feature name="openhab-binding-generacmobilelink" description="Generac MobileLink Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.generacmobilelink/${project.version}</bundle>
</feature>
</features>

View File

@ -23,7 +23,26 @@ import org.openhab.core.thing.ThingTypeUID;
*/
@NonNullByDefault
public class GeneracMobileLinkBindingConstants {
private static final String BINDING_ID = "generacmobilelink";
public static final String BINDING_ID = "generacmobilelink";
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_GENERATOR = new ThingTypeUID(BINDING_ID, "generator");
public static final String PROPERTY_GENERATOR_ID = "generatorId";
public static final String CHANNEL_HERO_IMAGE_URL = "heroImageUrl";
public static final String CHANNEL_STATUS_LABEL = "statusLabel";
public static final String CHANNEL_STATUS_TEXT = "statusText";
public static final String CHANNEL_ACTIVATION_DATE = "activationDate";
public static final String CHANNEL_DEVICE_SSID = "deviceSsid";
public static final String CHANNEL_STATUS = "status";
public static final String CHANNEL_IS_CONNECTED = "isConnected";
public static final String CHANNEL_IS_CONNECTING = "isConnecting";
public static final String CHANNEL_SHOW_WARNING = "showWarning";
public static final String CHANNEL_HAS_MAINTENANCE_ALERT = "hasMaintenanceAlert";
public static final String CHANNEL_LAST_SEEN = "lastSeen";
public static final String CHANNEL_CONNECTION_TIME = "connectionTime";
public static final String CHANNEL_RUN_HOURS = "runHours";
public static final String CHANNEL_BATTERY_VOLTAGE = "batteryVoltage";
public static final String CHANNEL_HOURS_OF_PROTECTION = "hoursOfProtection";
public static final String CHANNEL_SIGNAL_STRENGH = "signalStrength";
}

View File

@ -10,16 +10,17 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.generacmobilelink.internal.dto;
package org.openhab.binding.generacmobilelink.internal.config;
import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link GeneratorStatusResponseDTO} response from the MobileLink API
* The {@link GeneracMobileLinkGeneratorConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Dan Cunningham - Initial contribution
*/
@SuppressWarnings("serial")
public class GeneratorStatusResponseDTO extends ArrayList<GeneratorStatusDTO> {
@NonNullByDefault
public class GeneracMobileLinkGeneratorConfiguration {
public String generatorId = "";
}

View File

@ -12,21 +12,21 @@
*/
package org.openhab.binding.generacmobilelink.internal.discovery;
import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR;
import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants;
import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO;
import org.openhab.binding.generacmobilelink.internal.dto.Apparatus;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* The {@link GeneracMobileLinkDiscoveryService} is responsible for discovering generator things
* The {@link GeneracMobileLinkDiscoveryService} is responsible for discovering device things
*
* @author Dan Cunningham - Initial contribution
*/
@ -52,13 +52,13 @@ public class GeneracMobileLinkDiscoveryService extends AbstractDiscoveryService
return false;
}
public void generatorDiscovered(GeneratorStatusDTO generator, ThingUID bridgeUID) {
public void generatorDiscovered(Apparatus apparatus, ThingUID bridgeUID) {
DiscoveryResult result = DiscoveryResultBuilder
.create(new ThingUID(GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR, bridgeUID,
String.valueOf(generator.gensetID)))
.withLabel("MobileLink Generator " + generator.generatorName)
.withProperty("generatorId", String.valueOf(generator.gensetID))
.withRepresentationProperty("generatorId").withBridge(bridgeUID).build();
.create(new ThingUID(THING_TYPE_GENERATOR, bridgeUID, String.valueOf(apparatus.apparatusId)))
.withLabel("MobileLink Generator " + apparatus.name)
.withProperty(Thing.PROPERTY_SERIAL_NUMBER, String.valueOf(apparatus.serialNumber))
.withProperty(PROPERTY_GENERATOR_ID, String.valueOf(apparatus.apparatusId))
.withRepresentationProperty(PROPERTY_GENERATOR_ID).withBridge(bridgeUID).build();
thingDiscovered(result);
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
/**
* The {@link Account} represents a Generac Account
*
* @author Dan Cunningham - Initial contribution
*/
public class Account {
public String userId;
public String firstName;
public String lastName;
public String[] emails;
public String[] phoneNumbers;
public String[] groups;
public MobileLinkSettings mobileLinkSettings;
public class MobileLinkSettings {
public DisplaySettings displaySettings;
public class DisplaySettings {
public String distanceUom;
public String temperatureUom;
}
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
import java.util.List;
/**
* The {@link Apparatus} represents a Generac Apparatus (Generator)
*
* @author Dan Cunningham - Initial contribution
*/
public class Apparatus {
public int apparatusId;
public String serialNumber;
public String name;
public int type;
public String localizedAddress;
public String materialDescription;
public String heroImageUrl;
public int apparatusStatus;
public boolean isConnected;
public boolean isConnecting;
public boolean showWarning;
public Weather weather;
public String preferredDealerName;
public String preferredDealerPhone;
public String preferredDealerEmail;
public boolean isDealerManaged;
public boolean isDealerUnmonitored;
public String modelNumber;
public String panelId;
public List<Property> properties;
public class Weather {
public Temperature temperature;
public int iconCode;
public class Temperature {
public double value;
public String unit;
public int unitType;
}
}
public class Property {
public String name;
public Value value;
public int type;
public class Value {
public int type;
public String status;
public boolean isLegacy;
public boolean isDunning;
public String deviceId;
public String deviceType;
public String signalStrength;
public String batteryLevel;
}
}
public class Device {
public String deviceId;
public String deviceType;
public String signalStrength;
public String batteryLevel;
public String status;
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
import java.time.ZonedDateTime;
/**
* The {@link ApparatusDetail} represents the details of a Generac Apparatus
*
* @author Dan Cunningham - Initial contribution
*/
public class ApparatusDetail {
public int apparatusId;
public String name;
public String serialNumber;
public int apparatusClassification;
public String panelId;
public ZonedDateTime activationDate;
public String deviceType;
public String deviceSsid;
public String shortDeviceId;
public int apparatusStatus;
public String heroImageUrl;
public String statusLabel;
public String statusText;
public String eCodeLabel;
public Weather weather;
public boolean isConnected;
public boolean isConnecting;
public boolean showWarning;
public boolean hasMaintenanceAlert;
public ZonedDateTime lastSeen;
public String connectionTimestamp;
public Address address;
public Property[] properties;
public Subscription subscription;
public boolean enrolledInVpp;
public boolean hasActiveVppEvent;
public ProductInfo[] productInfo;
public boolean hasDisconnectedNotificationsOn;
public class Weather {
public Temperature temperature;
public int iconCode;
public class Temperature {
public double value;
public String unit;
public int unitType;
}
}
public class Address {
public String line1;
public String line2;
public String city;
public String region;
public String country;
public String postalCode;
}
public class Property {
public String name;
public String value;
public int type;
}
public class Subscription {
public int type;
public int status;
public boolean isLegacy;
public boolean isDunning;
}
public class ProductInfo {
public String name;
public String value;
public int type;
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
/**
* The {@link ApparatusInfo} represents the info of a Generac Apparatus
*
* @author Dan Cunningham - Initial contribution
*/
public class ApparatusInfo {
public int apparatusId;
public String apparatusName;
public String productType;
public String description;
public Property[] properties;
public Attribute[] attributes;
public class Property {
public String name;
public String value;
public int type;
}
public class Attribute {
public String name;
public String value;
public int type;
}
}

View File

@ -1,54 +0,0 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
/**
* {@link GeneratorStatusDTO} object from the MobileLink API
*
* @author Dan Cunningham - Initial contribution
*/
public class GeneratorStatusDTO {
public Integer gensetID;
public String generatorDate;
public String generatorName;
public String generatorSerialNumber;
public String generatorModel;
public String generatorDescription;
public String generatorMDN;
public String generatorImei;
public String generatorIccid;
public String generatorTetherSerial;
public Boolean connected;
public Boolean greenLightLit;
public Boolean yellowLightLit;
public Boolean redLightLit;
public Boolean blueLightLit;
public String generatorStatus;
public String generatorStatusDate;
public String currentAlarmDescription;
public Integer runHours;
public Integer exerciseHours;
public String batteryVoltage;
public Integer fuelType;
public Integer fuelLevel;
public String generatorBrandImageURL;
public Boolean generatorServiceStatus;
public String signalStrength;
public String deviceId;
public Integer deviceTypeId;
public String firmwareVersion;
public String timezone;
public String mACAddress;
public String iPAddress;
public String sSID;
}

View File

@ -1,31 +0,0 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
/**
* {@link LoginRequestDTO} request for the MobileLink API
*
* @author Dan Cunningham - Initial contribution
*/
public class LoginRequestDTO {
public LoginRequestDTO(String sharedKey, String userLogin, String userPassword) {
super();
this.sharedKey = sharedKey;
this.userLogin = userLogin;
this.userPassword = userPassword;
}
public String sharedKey;
public String userLogin;
public String userPassword;
}

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
/**
* {@link LoginResponseDTO} response from the MobileLink API
*
* @author Dan Cunningham - Initial contribution
*/
public class LoginResponseDTO {
public String authToken;
public String pushChannelName;
}

View File

@ -13,11 +13,12 @@
package org.openhab.binding.generacmobilelink.internal.dto;
/**
* {@link ErrorResponseDTO} object from the MobileLink API
* The {@link SelfAssertedResponse} represents the SelfAssertedResponse object used in login
*
* @author Dan Cunningham - Initial contribution
*/
public class ErrorResponseDTO {
public Integer errorCode;
public String errorMessage;
public class SelfAssertedResponse {
public String status;
public String errorCode;
public String message;
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2023 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.generacmobilelink.internal.dto;
import java.util.Map;
/**
* /**
* The {@link SignInConfig} represents the SignInConfig object used in login
*
* @author Dan Cunningham - Initial contribution
*/
public class SignInConfig {
public String remoteResource;
public int retryLimit;
public boolean trimSpacesInPassword;
public String api;
public String csrf;
public String transId;
public String pageViewId;
public boolean suppressElementCss;
public boolean isPageViewIdSentWithHeader;
public boolean allowAutoFocusOnPasswordField;
public int pageMode;
public Map<String, String> config;
public Map<String, String> hosts;
public Locale locale;
public XhrSettings xhrSettings;
public class Locale {
public String lang;
}
public class XhrSettings {
public boolean retryEnabled;
public int retryMaxAttempts;
public int retryDelay;
public int retryExponent;
public String[] retryOn;
}
}

View File

@ -21,7 +21,6 @@ import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.generacmobilelink.internal.discovery.GeneracMobileLinkDiscoveryService;
import org.openhab.binding.generacmobilelink.internal.handler.GeneracMobileLinkAccountHandler;
import org.openhab.binding.generacmobilelink.internal.handler.GeneracMobileLinkGeneratorHandler;
@ -51,11 +50,11 @@ public class GeneracMobileLinkHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
THING_TYPE_GENERATOR);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new ConcurrentHashMap<>();
private final HttpClient httpClient;
private final HttpClientFactory httpClientFactory;
@Activate
public GeneracMobileLinkHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.httpClientFactory = httpClientFactory;
}
@Override
@ -74,7 +73,7 @@ public class GeneracMobileLinkHandlerFactory extends BaseThingHandlerFactory {
if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
GeneracMobileLinkDiscoveryService discoveryService = new GeneracMobileLinkDiscoveryService();
GeneracMobileLinkAccountHandler accountHandler = new GeneracMobileLinkAccountHandler((Bridge) thing,
httpClient, discoveryService);
httpClientFactory, discoveryService);
discoveryServiceRegs.put(accountHandler.getThing().getUID(), bundleContext
.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
return accountHandler;

View File

@ -12,36 +12,42 @@
*/
package org.openhab.binding.generacmobilelink.internal.handler;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.util.Fields;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants;
import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkAccountConfiguration;
import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkGeneratorConfiguration;
import org.openhab.binding.generacmobilelink.internal.discovery.GeneracMobileLinkDiscoveryService;
import org.openhab.binding.generacmobilelink.internal.dto.ErrorResponseDTO;
import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO;
import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusResponseDTO;
import org.openhab.binding.generacmobilelink.internal.dto.LoginRequestDTO;
import org.openhab.binding.generacmobilelink.internal.dto.LoginResponseDTO;
import org.openhab.binding.generacmobilelink.internal.dto.Apparatus;
import org.openhab.binding.generacmobilelink.internal.dto.ApparatusDetail;
import org.openhab.binding.generacmobilelink.internal.dto.SelfAssertedResponse;
import org.openhab.binding.generacmobilelink.internal.dto.SignInConfig;
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.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
@ -49,9 +55,10 @@ import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonSyntaxException;
/**
* The {@link GeneracMobileLinkAccountHandler} is responsible for connecting to the MobileLink cloud service and
@ -61,191 +68,327 @@ import com.google.gson.GsonBuilder;
*/
@NonNullByDefault
public class GeneracMobileLinkAccountHandler extends BaseBridgeHandler {
private static final String BASE_URL = "https://api.mobilelinkgen.com";
private static final String SHARED_KEY = "GeneseeDepot13";
private final Logger logger = LoggerFactory.getLogger(GeneracMobileLinkAccountHandler.class);
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
private @Nullable Future<?> pollFuture;
private @Nullable String authToken;
private @Nullable GeneratorStatusResponseDTO generators;
private GeneracMobileLinkDiscoveryService discoveryService;
private HttpClient httpClient;
private int refreshIntervalSeconds = 60;
public GeneracMobileLinkAccountHandler(Bridge bridge, HttpClient httpClient,
private static final String API_BASE = "https://app.mobilelinkgen.com/api";
private static final String LOGIN_BASE = "https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn";
private static final Pattern SETTINGS_PATTERN = Pattern.compile("^var SETTINGS = (.*);$", Pattern.MULTILINE);
private static final Gson GSON = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
return ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString());
}).create();
private HttpClient httpClient;
private GeneracMobileLinkDiscoveryService discoveryService;
private Map<String, Apparatus> apparatusesCache = new HashMap<String, Apparatus>();
private int refreshIntervalSeconds = 60;
private boolean loggedIn;
private @Nullable Future<?> pollFuture;
public GeneracMobileLinkAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory,
GeneracMobileLinkDiscoveryService discoveryService) {
super(bridge);
this.httpClient = httpClient;
this.discoveryService = discoveryService;
httpClient = httpClientFactory.createHttpClient(GeneracMobileLinkBindingConstants.BINDING_ID);
httpClient.setFollowRedirects(true);
// We have to send a very large amount of cookies which exceeds the default buffer size
httpClient.setRequestBufferSize(16348);
try {
httpClient.start();
} catch (Exception e) {
throw new IllegalStateException("Error starting custom HttpClient", e);
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
authToken = null;
restartPoll();
stopOrRestartPoll(true);
}
@Override
public void dispose() {
stopPoll();
stopOrRestartPoll(false);
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("Could not stop HttpClient", e);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateGeneratorThings();
try {
updateGeneratorThings();
} catch (IOException | SessionExpiredException e) {
logger.debug("Could refresh things", e);
}
}
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
GeneratorStatusResponseDTO generatorsLocal = generators;
if (generatorsLocal != null) {
Optional<GeneratorStatusDTO> generatorOpt = generatorsLocal.stream()
.filter(g -> String.valueOf(g.gensetID).equals(childThing.getUID().getId())).findFirst();
if (generatorOpt.isPresent()) {
((GeneracMobileLinkGeneratorHandler) childHandler).updateGeneratorStatus(generatorOpt.get());
}
logger.debug("childHandlerInitialized {}", childThing.getUID());
String id = childThing.getConfiguration().as(GeneracMobileLinkGeneratorConfiguration.class).generatorId;
Apparatus apparatus = apparatusesCache.get(id);
if (apparatus == null) {
logger.debug("No device for id {}", id);
return;
}
try {
updateGeneratorThing(childHandler, apparatus);
} catch (IOException | SessionExpiredException e) {
logger.debug("Could not initialize child", e);
}
}
private void stopPoll() {
Future<?> localPollFuture = pollFuture;
if (localPollFuture != null) {
localPollFuture.cancel(true);
private synchronized void stopOrRestartPoll(boolean restart) {
Future<?> pollFuture = this.pollFuture;
if (pollFuture != null) {
pollFuture.cancel(true);
this.pollFuture = null;
}
if (restart) {
this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 1, refreshIntervalSeconds, TimeUnit.SECONDS);
}
}
private void restartPoll() {
stopPoll();
pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshIntervalSeconds, TimeUnit.SECONDS);
}
private void poll() {
try {
if (authToken == null) {
logger.debug("Attempting Login");
if (!loggedIn) {
login();
}
getStatuses(true);
} catch (InterruptedException e) {
loggedIn = true;
updateGeneratorThings();
} catch (IOException e) {
logger.debug("Could not update devices", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/thing.generacmobilelink.account.offline.communication-error.io-exception");
} catch (SessionExpiredException e) {
logger.debug("Session expired", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/thing.generacmobilelink.account.offline.communication-error.session-expired");
loggedIn = false;
} catch (InvalidCredentialsException e) {
logger.debug("Credentials Invalid", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/thing.generacmobilelink.account.offline.configuration-error.invalid-credentials");
loggedIn = false;
// we don't want to continue polling with bad credentials
stopOrRestartPoll(false);
}
}
private synchronized void login() throws InterruptedException {
GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class);
refreshIntervalSeconds = config.refreshInterval;
HTTPResult result = sendRequest(BASE_URL + "/Users/login", HttpMethod.POST, null,
new StringContentProvider(
gson.toJson(new LoginRequestDTO(SHARED_KEY, config.username, config.password))),
"application/json");
if (result.responseCode == HttpStatus.OK_200) {
LoginResponseDTO loginResponse = gson.fromJson(result.content, LoginResponseDTO.class);
if (loginResponse != null) {
authToken = loginResponse.authToken;
updateStatus(ThingStatus.ONLINE);
}
} else {
handleErrorResponse(result);
if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
// bad credentials, stop trying to login
stopPoll();
}
}
}
private void getStatuses(boolean retry) throws InterruptedException {
if (authToken == null) {
private void updateGeneratorThings() throws IOException, SessionExpiredException {
Apparatus[] apparatuses = getEndpoint(Apparatus[].class, "/v2/Apparatus/list");
if (apparatuses == null) {
logger.debug("Could not decode apparatuses response");
return;
}
HTTPResult result = sendRequest(BASE_URL + "/Generator/GeneratorStatus", HttpMethod.GET, authToken, null, null);
if (result.responseCode == HttpStatus.OK_200) {
generators = gson.fromJson(result.content, GeneratorStatusResponseDTO.class);
updateGeneratorThings();
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
for (Apparatus apparatus : apparatuses) {
if (apparatus.type != 0) {
logger.debug("Unknown apparatus type {} {}", apparatus.type, apparatus.name);
continue;
}
} else {
if (retry) {
logger.debug("Retrying status request");
getStatuses(false);
String id = String.valueOf(apparatus.apparatusId);
apparatusesCache.put(id, apparatus);
Optional<Thing> thing = getThing().getThings().stream().filter(
t -> t.getConfiguration().as(GeneracMobileLinkGeneratorConfiguration.class).generatorId.equals(id))
.findFirst();
if (!thing.isPresent()) {
discoveryService.generatorDiscovered(apparatus, getThing().getUID());
} else {
handleErrorResponse(result);
ThingHandler handler = thing.get().getHandler();
if (handler != null) {
updateGeneratorThing(handler, apparatus);
}
}
}
}
private HTTPResult sendRequest(String url, HttpMethod method, @Nullable String token,
@Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException {
private void updateGeneratorThing(ThingHandler handler, Apparatus apparatus)
throws IOException, SessionExpiredException {
ApparatusDetail detail = getEndpoint(ApparatusDetail.class, "/v1/Apparatus/details/" + apparatus.apparatusId);
if (detail != null) {
((GeneracMobileLinkGeneratorHandler) handler).updateGeneratorStatus(apparatus, detail);
} else {
logger.debug("Could not decode apparatuses detail response");
}
}
private @Nullable <T> T getEndpoint(Class<T> clazz, String endpoint) throws IOException, SessionExpiredException {
try {
Request request = httpClient.newRequest(url).method(method).timeout(10, TimeUnit.SECONDS);
if (token != null) {
request = request.header("AuthToken", token);
ContentResponse response = httpClient.newRequest(API_BASE + endpoint).send();
if (response.getStatus() == 204) {
// no data
return null;
}
if (content != null & contentType != null) {
request = request.content(content, contentType);
if (response.getStatus() != 200) {
throw new SessionExpiredException("API returned status code: " + response.getStatus());
}
logger.trace("Sending {} to {}", request.getMethod(), request.getURI());
final CompletableFuture<HTTPResult> futureResult = new CompletableFuture<>();
request.send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(Result result) {
futureResult.complete(new HTTPResult(result.getResponse().getStatus(), getContentAsString()));
}
});
HTTPResult result = futureResult.get();
logger.trace("Response - status: {} content: {}", result.responseCode, result.content);
return result;
} catch (ExecutionException e) {
return new HTTPResult(0, e.getMessage());
String data = response.getContentAsString();
logger.debug("getEndpoint {}", data);
return GSON.fromJson(data, clazz);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (TimeoutException | ExecutionException | JsonSyntaxException e) {
throw new IOException(e);
}
}
private void handleErrorResponse(HTTPResult result) {
switch (result.responseCode) {
case HttpStatus.UNAUTHORIZED_401:
// the server responds with a 500 error in some cases when credentials are not correct
case HttpStatus.INTERNAL_SERVER_ERROR_500:
// server returned a valid error response
ErrorResponseDTO error = gson.fromJson(result.content, ErrorResponseDTO.class);
if (error != null && error.errorCode > 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unauthorized: " + result.content);
authToken = null;
break;
}
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, result.content);
/**
* Attempts to login through a Microsoft Azure implicit grant oauth flow
*
* @throws IOException if there is a problem communicating or parsing the responses
* @throws InvalidCredentialsException If Azure rejects the login credentials.
*/
private synchronized void login() throws IOException, InvalidCredentialsException {
logger.debug("Attempting login");
GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class);
refreshIntervalSeconds = config.refreshInterval;
try {
ContentResponse signInResponse = httpClient.newRequest(API_BASE + "/Auth/SignIn?email=" + config.username)
.send();
String responseData = signInResponse.getContentAsString();
logger.trace("response data: {}", responseData);
// If we are immediately returned a submit form, it means our cookies are still valid with the identity
// provider and we can just try and submit to the API service
if (submitPage(responseData)) {
return;
}
// Azure wants us to login again, look for the SETTINGS javascript in the page
Matcher matcher = SETTINGS_PATTERN.matcher(responseData);
if (!matcher.find()) {
throw new IOException("Could not find settings string");
}
String parseSettings = matcher.group(1);
logger.debug("parseSettings: {}", parseSettings);
SignInConfig signInConfig = GSON.fromJson(parseSettings, SignInConfig.class);
if (signInConfig == null) {
throw new IOException("Could not parse settings string");
}
Fields fields = new Fields();
fields.put("request_type", "RESPONSE");
fields.put("signInName", config.username);
fields.put("password", config.password);
Request selfAssertedRequest = httpClient.POST(LOGIN_BASE + "/SelfAsserted")
.header("X-Csrf-Token", signInConfig.csrf).param("tx", "StateProperties=" + signInConfig.transId)
.param("p", "B2C_1A_SignUpOrSigninOnline").content(new FormContentProvider(fields));
ContentResponse selfAssertedResponse = selfAssertedRequest.send();
logger.debug("selfAssertedRequest response {}", selfAssertedResponse.getStatus());
if (selfAssertedResponse.getStatus() != 200) {
throw new IOException("SelfAsserted: Bad response status: " + selfAssertedResponse.getStatus());
}
SelfAssertedResponse sa = GSON.fromJson(selfAssertedResponse.getContentAsString(),
SelfAssertedResponse.class);
if (sa == null) {
throw new IOException("SelfAsserted Could not parse response JSON");
}
if (!"200".equals(sa.status)) {
throw new InvalidCredentialsException("Invalid Credentials: " + sa.message);
}
Request confirmedRequest = httpClient.newRequest(LOGIN_BASE + "/api/CombinedSigninAndSignup/confirmed")
.param("csrf_token", signInConfig.csrf).param("tx", "StateProperties=" + signInConfig.transId)
.param("p", "B2C_1A_SignUpOrSigninOnline");
ContentResponse confirmedResponse = confirmedRequest.send();
if (confirmedResponse.getStatus() != 200) {
throw new IOException("CombinedSigninAndSignup bad response: " + confirmedResponse.getStatus());
}
String loginString = confirmedResponse.getContentAsString();
logger.trace("confirmedResponse: {}", loginString);
if (!submitPage(loginString)) {
throw new IOException("Error parsing HTML submit form");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException | TimeoutException | JsonSyntaxException e) {
throw new IOException(e);
}
}
private void updateGeneratorThings() {
GeneratorStatusResponseDTO generatorsLocal = generators;
if (generatorsLocal != null) {
generatorsLocal.forEach(generator -> {
Thing thing = getThing().getThing(new ThingUID(GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR,
getThing().getUID(), String.valueOf(generator.gensetID)));
if (thing == null) {
discoveryService.generatorDiscovered(generator, getThing().getUID());
} else {
ThingHandler handler = thing.getHandler();
if (handler != null) {
((GeneracMobileLinkGeneratorHandler) handler).updateGeneratorStatus(generator);
}
}
});
/**
* Attempts to submit a HTML form from Azure to the Generac API, returns false if the HTML does not match the
* required form
*
* @param loginString
* @return false if the HTML is not a form, true if submission is successful
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
* @throws JsonSyntaxException
* @throws IOException
*/
private boolean submitPage(String loginString)
throws ExecutionException, TimeoutException, InterruptedException, JsonSyntaxException, IOException {
Document loginPage = Jsoup.parse(loginString);
Element form = loginPage.select("form").first();
Element loginState = loginPage.select("input[name=state]").first();
Element loginCode = loginPage.select("input[name=code]").first();
if (form == null || loginState == null || loginCode == null) {
logger.debug("Could not load login page");
return false;
}
// url that the form will submit to
String action = form.attr("action");
Fields fields = new Fields();
fields.put("state", loginState.attr("value"));
fields.put("code", loginCode.attr("value"));
Request loginRequest = httpClient.POST(action).content(new FormContentProvider(fields));
ContentResponse loginResponse = loginRequest.send();
if (logger.isTraceEnabled()) {
logger.trace("login response {} {}", loginResponse.getStatus(), loginResponse.getContentAsString());
} else {
logger.debug("login response status {}", loginResponse.getStatus());
}
if (loginResponse.getStatus() != 200) {
throw new IOException("Bad api login resposne: " + loginResponse.getStatus());
}
return true;
}
private class InvalidCredentialsException extends Exception {
private static final long serialVersionUID = 1L;
public InvalidCredentialsException(String message) {
super(message);
}
}
public static class HTTPResult {
public @Nullable String content;
public final int responseCode;
private class SessionExpiredException extends Exception {
private static final long serialVersionUID = 1L;
public HTTPResult(int responseCode, @Nullable String content) {
this.responseCode = responseCode;
this.content = content;
public SessionExpiredException(String message) {
super(message);
}
}
}

View File

@ -12,16 +12,18 @@
*/
package org.openhab.binding.generacmobilelink.internal.handler;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.*;
import java.util.Arrays;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.ElectricPotential;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO;
import org.openhab.binding.generacmobilelink.internal.dto.Apparatus;
import org.openhab.binding.generacmobilelink.internal.dto.ApparatusDetail;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -34,6 +36,7 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -45,7 +48,9 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class GeneracMobileLinkGeneratorHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(GeneracMobileLinkGeneratorHandler.class);
private @Nullable GeneratorStatusDTO status;
private @Nullable Apparatus apparatus;
private @Nullable ApparatusDetail apparatusDetail;
public GeneracMobileLinkGeneratorHandler(Thing thing) {
super(thing);
@ -63,37 +68,66 @@ public class GeneracMobileLinkGeneratorHandler extends BaseThingHandler {
updateStatus(ThingStatus.UNKNOWN);
}
protected void updateGeneratorStatus(GeneratorStatusDTO status) {
this.status = status;
protected void updateGeneratorStatus(Apparatus apparatus, ApparatusDetail apparatusDetail) {
this.apparatus = apparatus;
this.apparatusDetail = apparatusDetail;
updateStatus(ThingStatus.ONLINE);
updateState();
}
protected void updateState() {
final GeneratorStatusDTO localStatus = status;
if (localStatus != null) {
updateState("connected", OnOffType.from(localStatus.connected));
updateState("greenLight", OnOffType.from(localStatus.greenLightLit));
updateState("yellowLight", OnOffType.from(localStatus.yellowLightLit));
updateState("redLight", OnOffType.from(localStatus.redLightLit));
updateState("blueLight", OnOffType.from(localStatus.blueLightLit));
try {
// API returns a format like 12/20/2020
updateState("statusDate",
new DateTimeType(LocalDate
.parse(localStatus.generatorStatusDate, DateTimeFormatter.ofPattern("MM/dd/yyyy"))
.atStartOfDay(ZoneId.systemDefault())));
} catch (IllegalArgumentException | DateTimeParseException e) {
logger.debug("Could not parse statusDate", e);
}
updateState("status", new StringType(localStatus.generatorStatus));
updateState("currentAlarmDescription", new StringType(localStatus.currentAlarmDescription));
updateState("runHours", new QuantityType<Time>(localStatus.runHours, Units.HOUR));
updateState("exerciseHours", new QuantityType<Time>(localStatus.exerciseHours, Units.HOUR));
updateState("fuelType", new DecimalType(localStatus.fuelType));
updateState("fuelLevel", QuantityType.valueOf(localStatus.fuelLevel, Units.PERCENT));
updateState("batteryVoltage", new StringType(localStatus.batteryVoltage));
updateState("serviceStatus", OnOffType.from(localStatus.generatorServiceStatus));
private void updateState() {
Apparatus apparatus = this.apparatus;
ApparatusDetail apparatusDetail = this.apparatusDetail;
if (apparatus == null || apparatusDetail == null) {
return;
}
updateState(CHANNEL_HERO_IMAGE_URL, new StringType(apparatusDetail.heroImageUrl));
updateState(CHANNEL_STATUS_LABEL, new StringType(apparatusDetail.statusLabel));
updateState(CHANNEL_STATUS_TEXT, new StringType(apparatusDetail.statusText));
updateState(CHANNEL_ACTIVATION_DATE, new DateTimeType(apparatusDetail.activationDate));
updateState(CHANNEL_DEVICE_SSID, new StringType(apparatusDetail.deviceSsid));
updateState(CHANNEL_STATUS, new DecimalType(apparatusDetail.apparatusStatus));
updateState(CHANNEL_IS_CONNECTED, OnOffType.from(apparatusDetail.isConnected));
updateState(CHANNEL_IS_CONNECTING, OnOffType.from(apparatusDetail.isConnecting));
updateState(CHANNEL_SHOW_WARNING, OnOffType.from(apparatusDetail.showWarning));
updateState(CHANNEL_HAS_MAINTENANCE_ALERT, OnOffType.from(apparatusDetail.hasMaintenanceAlert));
updateState(CHANNEL_LAST_SEEN, new DateTimeType(apparatusDetail.lastSeen));
updateState(CHANNEL_CONNECTION_TIME, new DateTimeType(apparatusDetail.connectionTimestamp));
Arrays.stream(apparatusDetail.properties).filter(p -> p.type == 70).findFirst().ifPresent(p -> {
try {
updateState(CHANNEL_RUN_HOURS, new QuantityType<Time>(Integer.parseInt(p.value), Units.HOUR));
} catch (NumberFormatException e) {
logger.debug("Could not parse runHours {}", p.value);
updateState(CHANNEL_RUN_HOURS, UnDefType.UNDEF);
}
});
Arrays.stream(apparatusDetail.properties).filter(p -> p.type == 69).findFirst().ifPresent(p -> {
try {
updateState(CHANNEL_BATTERY_VOLTAGE,
new QuantityType<ElectricPotential>(Float.parseFloat(p.value), Units.VOLT));
} catch (NumberFormatException e) {
logger.debug("Could not parse batteryVoltage {}", p.value);
updateState(CHANNEL_BATTERY_VOLTAGE, UnDefType.UNDEF);
}
});
Arrays.stream(apparatusDetail.properties).filter(p -> p.type == 31).findFirst().ifPresent(p -> {
try {
updateState(CHANNEL_HOURS_OF_PROTECTION, new QuantityType<Time>(Float.parseFloat(p.value), Units.HOUR));
} catch (NumberFormatException e) {
logger.debug("Could not parse hoursOfProtection {}", p.value);
updateState(CHANNEL_HOURS_OF_PROTECTION, UnDefType.UNDEF);
}
});
apparatus.properties.stream().filter(p -> p.type == 3).findFirst().ifPresent(p -> {
try {
if (p.value.signalStrength != null) {
updateState(CHANNEL_SIGNAL_STRENGH, new QuantityType<Dimensionless>(
Integer.parseInt(p.value.signalStrength.replaceAll("%", "")), Units.PERCENT));
}
} catch (NumberFormatException e) {
logger.debug("Could not parse signalStrength {}", p.value.signalStrength);
updateState(CHANNEL_SIGNAL_STRENGH, UnDefType.UNDEF);
}
});
}
}

View File

@ -6,5 +6,6 @@
<type>binding</type>
<name>GeneracMobileLink Binding</name>
<description>This binding monitors Generac manufactured generators through the MobileLink cloud service.</description>
<connection>cloud</connection>
</addon:addon>

View File

@ -23,17 +23,48 @@ thing-type.config.generacmobilelink.generator.generatorId.description = Generato
# channel types
channel-type.generacmobilelink.batteryVoltage.label = Battery Voltage Status
channel-type.generacmobilelink.blueLight.label = Blue Light Status
channel-type.generacmobilelink.connected.label = Connected
channel-type.generacmobilelink.currentAlarmDescription.label = Current Alarm Description
channel-type.generacmobilelink.exerciseHours.label = Number of Hours Exercised
channel-type.generacmobilelink.fuelLevel.label = Fuel Level
channel-type.generacmobilelink.fuelType.label = Fuel Type
channel-type.generacmobilelink.greenLight.label = Green Light Status
channel-type.generacmobilelink.redLight.label = Red Light Status
channel-type.generacmobilelink.runHours.label = Number of Hours Run
channel-type.generacmobilelink.serviceStatus.label = Service Status
channel-type.generacmobilelink.activationDate.label = Activation Date
channel-type.generacmobilelink.activationDate.description = The activation date of the generator.
channel-type.generacmobilelink.batteryVoltage.label = Battery Voltage
channel-type.generacmobilelink.batteryVoltage.description = The battery voltage.
channel-type.generacmobilelink.connectionTime.label = Connection Time
channel-type.generacmobilelink.connectionTime.description = The date that the unit has been connected from.
channel-type.generacmobilelink.deviceSsid.label = Device SSID
channel-type.generacmobilelink.deviceSsid.description = The SSID that the generator broadcasts for setup.
channel-type.generacmobilelink.hasMaintenanceAlert.label = Has Maintenance Alert
channel-type.generacmobilelink.hasMaintenanceAlert.description = Does the generator require maintenance.
channel-type.generacmobilelink.heroImageUrl.label = Hero Image URL
channel-type.generacmobilelink.heroImageUrl.description = URL to an image of the generator.
channel-type.generacmobilelink.hoursOfProtection.label = Hours of Protection
channel-type.generacmobilelink.hoursOfProtection.description = Number of hours of protection the generator has provided.
channel-type.generacmobilelink.isConnected.label = Is Connected
channel-type.generacmobilelink.isConnected.description = Is the unit connected to the cloud service.
channel-type.generacmobilelink.isConnecting.label = Is Connecting
channel-type.generacmobilelink.isConnecting.description = Is the unit connecting to the cloud service.
channel-type.generacmobilelink.lastSeen.label = Last Seen
channel-type.generacmobilelink.lastSeen.description = The date that the unit was last connected to the cloud service.
channel-type.generacmobilelink.runHours.label = Run Hours
channel-type.generacmobilelink.runHours.description = Number of hours run.
channel-type.generacmobilelink.showWarning.label = Show Warning
channel-type.generacmobilelink.showWarning.description = Should a user interface show a warning symbol due to the current status.
channel-type.generacmobilelink.signalStrength.label = Signal Strength
channel-type.generacmobilelink.signalStrength.description = The Wi-Fi signal strength of the generator
channel-type.generacmobilelink.status.label = Status
channel-type.generacmobilelink.statusDate.label = Last Status Date
channel-type.generacmobilelink.yellowLight.label = Yellow Light Status
channel-type.generacmobilelink.status.description = The current status of the generator.
channel-type.generacmobilelink.status.state.option.1 = Ready
channel-type.generacmobilelink.status.state.option.2 = Running
channel-type.generacmobilelink.status.state.option.3 = Exercising
channel-type.generacmobilelink.status.state.option.4 = Warning
channel-type.generacmobilelink.status.state.option.5 = Stopped
channel-type.generacmobilelink.status.state.option.6 = Communication Issue
channel-type.generacmobilelink.status.state.option.7 = Unknown
channel-type.generacmobilelink.statusLabel.label = Status Label
channel-type.generacmobilelink.statusLabel.description = The label used to identify the current status.
channel-type.generacmobilelink.statusText.label = Status Text
channel-type.generacmobilelink.statusText.description = The longer description of the current status.
# things
thing.generacmobilelink.account.offline.communication-error.session-expired = Session Expired
thing.generacmobilelink.account.offline.configuration-error.invalid-credentials = Invalid Credentials
thing.generacmobilelink.account.offline.communication-error.io-exception = Error Communicating with Service

View File

@ -17,93 +17,131 @@
<label>MobileLink Generator</label>
<description>MobileLink Generator</description>
<channels>
<channel id="connected" typeId="connected"/>
<channel id="greenLight" typeId="greenLight"/>
<channel id="yellowLight" typeId="yellowLight"/>
<channel id="redLight" typeId="redLight"/>
<channel id="blueLight" typeId="blueLight"/>
<channel id="statusDate" typeId="statusDate"/>
<channel id="heroImageUrl" typeId="heroImageUrl"/>
<channel id="statusLabel" typeId="statusLabel"/>
<channel id="statusText" typeId="statusText"/>
<channel id="activationDate" typeId="activationDate"/>
<channel id="deviceSsid" typeId="deviceSsid"/>
<channel id="status" typeId="status"/>
<channel id="currentAlarmDescription" typeId="currentAlarmDescription"/>
<channel id="isConnected" typeId="isConnected"/>
<channel id="isConnecting" typeId="isConnecting"/>
<channel id="showWarning" typeId="showWarning"/>
<channel id="hasMaintenanceAlert" typeId="hasMaintenanceAlert"/>
<channel id="lastSeen" typeId="lastSeen"/>
<channel id="connectionTime" typeId="connectionTime"/>
<channel id="runHours" typeId="runHours"/>
<channel id="exerciseHours" typeId="exerciseHours"/>
<channel id="fuelType" typeId="fuelType"/>
<channel id="fuelLevel" typeId="fuelLevel"/>
<channel id="batteryVoltage" typeId="batteryVoltage"/>
<channel id="serviceStatus" typeId="serviceStatus"/>
<channel id="hoursOfProtection" typeId="hoursOfProtection"/>
<channel id="signalStrength" typeId="signalStrength"/>
</channels>
<representation-property>generatorId</representation-property>
<config-description-ref uri="thing-type:generacmobilelink:generator"/>
</thing-type>
<channel-type id="connected">
<item-type>Switch</item-type>
<label>Connected</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="greenLight">
<item-type>Switch</item-type>
<label>Green Light Status</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="yellowLight">
<item-type>Switch</item-type>
<label>Yellow Light Status</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="redLight">
<item-type>Switch</item-type>
<label>Red Light Status</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="blueLight">
<item-type>Switch</item-type>
<label>Blue Light Status</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="statusDate">
<item-type>DateTime</item-type>
<label>Last Status Date</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="status">
<item-type>String</item-type>
<item-type>Number</item-type>
<label>Status</label>
<description>The current status of the generator.</description>
<state readOnly="true">
<options>
<option value="1">Ready</option>
<option value="2">Running</option>
<option value="3">Exercising</option>
<option value="4">Warning</option>
<option value="5">Stopped</option>
<option value="6">Communication Issue</option>
<option value="7">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="statusLabel">
<item-type>String</item-type>
<label>Status Label</label>
<description>The label used to identify the current status.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="currentAlarmDescription">
<channel-type id="statusText">
<item-type>String</item-type>
<label>Current Alarm Description</label>
<label>Status Text</label>
<description>The longer description of the current status.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="heroImageUrl">
<item-type>String</item-type>
<label>Hero Image URL</label>
<description>URL to an image of the generator.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="activationDate">
<item-type>DateTime</item-type>
<label>Activation Date</label>
<description>The activation date of the generator.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="deviceSsid">
<item-type>String</item-type>
<label>Device SSID</label>
<description>The SSID that the generator broadcasts for setup.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="isConnected">
<item-type>Switch</item-type>
<label>Is Connected</label>
<description>Is the unit connected to the cloud service.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="isConnecting">
<item-type>Switch</item-type>
<label>Is Connecting</label>
<description>Is the unit connecting to the cloud service.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="showWarning">
<item-type>Switch</item-type>
<label>Show Warning</label>
<description>Should a user interface show a warning symbol due to the current status.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="hasMaintenanceAlert">
<item-type>Switch</item-type>
<label>Has Maintenance Alert</label>
<description>Does the generator require maintenance.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastSeen">
<item-type>DateTime</item-type>
<label>Last Seen</label>
<description>The date that the unit was last connected to the cloud service.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="connectionTime">
<item-type>DateTime</item-type>
<label>Connection Time</label>
<description>The date that the unit has been connected from.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="runHours">
<item-type>Number:Time</item-type>
<label>Number of Hours Run</label>
<label>Run Hours</label>
<description>Number of hours run.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="exerciseHours">
<item-type>Number:Time</item-type>
<label>Number of Hours Exercised</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="fuelType">
<item-type>Number</item-type>
<label>Fuel Type</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="fuelLevel">
<item-type>Number:Dimensionless</item-type>
<label>Fuel Level</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="batteryVoltage">
<item-type>String</item-type>
<label>Battery Voltage Status</label>
<state readOnly="true"/>
<item-type>Number:ElectricPotential</item-type>
<label>Battery Voltage</label>
<description>The battery voltage.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="serviceStatus">
<item-type>Switch</item-type>
<label>Service Status</label>
<state readOnly="true"/>
<channel-type id="hoursOfProtection">
<item-type>Number:Time</item-type>
<label>Hours of Protection</label>
<description>Number of hours of protection the generator has provided.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="signalStrength">
<item-type>Number:Dimensionless</item-type>
<label>Signal Strength</label>
<description>The Wi-Fi signal strength of the generator</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
</thing:thing-descriptions>