* This is the initial contribution of the Generac MobileLink binding. This allows the Generac line of generators using their MobileLink cloud service to be monitored as things in openHAB.
* bump to 3.1
Signed-off-by: Dan Cunningham <dan@digitaldan.com>
/bundles/org.openhab.binding.ftpupload/ @paulianttila
/bundles/org.openhab.binding.gardena/ @gerrieg
/bundles/org.openhab.binding.gce/ @clinique
+/bundles/org.openhab.binding.generacmobilelink/ @digitaldan
/bundles/org.openhab.binding.globalcache/ @mhilbush
/bundles/org.openhab.binding.goecharger/ @SamuelBrucksch
/bundles/org.openhab.binding.gpstracker/ @gbicskei
<artifactId>org.openhab.binding.gce</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.binding.generacmobilelink</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.globalcache</artifactId>
--- /dev/null
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
--- /dev/null
+# Generac MobileLink Binding
+
+This binding communicates with the Generac MobileLink API and reports on the status of Generac manufactured generators, including versions resold under the brands Eaton, Honeywell and Siemens.
+
+## Supported Things
+
+
+### MobileLink Account
+
+A MobileLink account bridge thing represents a user's MobileLink account and is responsible for authentication and polling for updates.
+
+ThingTypeUID: `account`
+
+### Generator
+
+A Generator thing represents a individual generator linked to an account bridge. Multiple generators are supported.
+
+ThingTypeUID: `generator`
+
+## Discovery
+
+The MobileLink account bridge must be added manually. Once added, generator things will automatically be added to the inbox.
+
+## Thing Configuration
+
+### MobileLink Account
+
+| Parameter | Description |
+|-----------------|------------------------------------------------------------------------------------|
+| username | The user name, typically an email address, used to login to the MobileLink service |
+| password | The password used to login to the MobileLink service |
+| refreshInterval | The frequency to poll for generator updates, minimum duration is 30 seconds |
+
+
+## Channels
+
+### Generator Channels
+
+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 |
+
+
+## Full Example
+
+### Things
+
+```xtend
+Bridge generacmobilelink:account:main "MobileLink Account" [ userName="foo@bar.com", password="secret",refreshInterval=60 ] {
+ Thing generator 123456 "MobileLink Generator" [ generatorId="123456" ]
+}
+```
+
+### Items
+
+```xtend
+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"}
+```
+
+### Sitemap
+
+```xtend
+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
+ }
+}
+```
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+ <version>3.1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>org.openhab.binding.generacmobilelink</artifactId>
+
+ <name>openHAB Add-ons :: Bundles :: GeneracMobileLink Binding</name>
+
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright (c) 2010-2020 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
+
+-->
+<features name="org.openhab.binding.generacmobilelink-${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-generacmobilelink" description="Generac MobileLink Binding" version="${project.version}">
+ <feature>openhab-runtime-base</feature>
+ <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.generacmobilelink/${project.version}</bundle>
+ </feature>
+</features>
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link GeneracMobileLinkBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class GeneracMobileLinkBindingConstants {
+ private 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");
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GeneracMobileLinkAccountConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class GeneracMobileLinkAccountConfiguration {
+ public String username = "";
+ public String password = "";
+ public Integer refreshInterval = 60;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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.discovery;
+
+import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR;
+
+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.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * The {@link GeneracMobileLinkDiscoveryService} is responsible for discovering generator things
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class GeneracMobileLinkDiscoveryService extends AbstractDiscoveryService {
+ private static final Set<ThingTypeUID> SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set.of(THING_TYPE_GENERATOR);
+
+ public GeneracMobileLinkDiscoveryService() {
+ super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 0);
+ }
+
+ @Override
+ public Set<ThingTypeUID> getSupportedThingTypes() {
+ return SUPPORTED_DISCOVERY_THING_TYPES_UIDS;
+ }
+
+ @Override
+ public void startScan() {
+ }
+
+ @Override
+ public boolean isBackgroundDiscoveryEnabled() {
+ return false;
+ }
+
+ public void generatorDiscovered(GeneratorStatusDTO generator, 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();
+ thingDiscovered(result);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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 ErrorResponseDTO} object from the MobileLink API
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class ErrorResponseDTO {
+ public Integer errorCode;
+ public String errorMessage;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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.ArrayList;
+
+/**
+ * {@link GeneratorStatusResponseDTO} response from the MobileLink API
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@SuppressWarnings("serial")
+public class GeneratorStatusResponseDTO extends ArrayList<GeneratorStatusDTO> {
+
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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.factory;
+
+import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.*;
+
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+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;
+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.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link GeneracMobileLinkHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.generacmobilelink", service = ThingHandlerFactory.class)
+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;
+
+ @Activate
+ public GeneracMobileLinkHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ }
+
+ @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_GENERATOR.equals(thingTypeUID)) {
+ return new GeneracMobileLinkGeneratorHandler(thing);
+ }
+
+ if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
+ GeneracMobileLinkDiscoveryService discoveryService = new GeneracMobileLinkDiscoveryService();
+ GeneracMobileLinkAccountHandler accountHandler = new GeneracMobileLinkAccountHandler((Bridge) thing,
+ httpClient, discoveryService);
+ discoveryServiceRegs.put(accountHandler.getThing().getUID(), bundleContext
+ .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
+ return accountHandler;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected synchronized void removeHandler(ThingHandler thingHandler) {
+ if (thingHandler instanceof GeneracMobileLinkAccountHandler) {
+ ServiceRegistration<?> serviceReg = discoveryServiceRegs.remove(thingHandler.getThing().getUID());
+ if (serviceReg != null) {
+ serviceReg.unregister();
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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.handler;
+
+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 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.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.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants;
+import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkAccountConfiguration;
+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.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;
+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;
+
+/**
+ * The {@link GeneracMobileLinkAccountHandler} is responsible for connecting to the MobileLink cloud service and
+ * discovering generator things
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@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,
+ GeneracMobileLinkDiscoveryService discoveryService) {
+ super(bridge);
+ this.httpClient = httpClient;
+ this.discoveryService = discoveryService;
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.UNKNOWN);
+ authToken = null;
+ restartPoll();
+ }
+
+ @Override
+ public void dispose() {
+ stopPoll();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateGeneratorThings();
+ }
+ }
+
+ @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());
+ }
+ }
+ }
+
+ private void stopPoll() {
+ Future<?> localPollFuture = pollFuture;
+ if (localPollFuture != null) {
+ localPollFuture.cancel(true);
+ }
+ }
+
+ private void restartPoll() {
+ stopPoll();
+ pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshIntervalSeconds, TimeUnit.SECONDS);
+ }
+
+ private void poll() {
+ try {
+ if (authToken == null) {
+ logger.debug("Attempting Login");
+ login();
+ }
+ getStatuses(true);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ 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) {
+ 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);
+ }
+ } else {
+ if (retry) {
+ logger.debug("Retrying status request");
+ getStatuses(false);
+ } else {
+ handleErrorResponse(result);
+ }
+ }
+ }
+
+ private HTTPResult sendRequest(String url, HttpMethod method, @Nullable String token,
+ @Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException {
+ try {
+ Request request = httpClient.newRequest(url).method(method).timeout(10, TimeUnit.SECONDS);
+ if (token != null) {
+ request = request.header("AuthToken", token);
+ }
+ if (content != null & contentType != null) {
+ request = request.content(content, contentType);
+ }
+ 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());
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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);
+ }
+ }
+ });
+ }
+ }
+
+ public static class HTTPResult {
+ public @Nullable String content;
+ public final int responseCode;
+
+ public HTTPResult(int responseCode, @Nullable String content) {
+ this.responseCode = responseCode;
+ this.content = content;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 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.handler;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+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.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.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link GeneracMobileLinkGeneratorHandler} is responsible for updating a generator things's channels
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class GeneracMobileLinkGeneratorHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(GeneracMobileLinkGeneratorHandler.class);
+ private @Nullable GeneratorStatusDTO status;
+
+ public GeneracMobileLinkGeneratorHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateState();
+ }
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ protected void updateGeneratorStatus(GeneratorStatusDTO status) {
+ this.status = status;
+ 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));
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="generacmobilelink" 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>GeneracMobileLink Binding</name>
+ <description>This binding monitors Generac manufactured generators through the MobileLink cloud service.</description>
+
+</binding:binding>
--- /dev/null
+<config-description:config-descriptions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+ <config-description uri="thing-type:generacmobilelink:account">
+ <parameter name="username" type="text" required="true">
+ <label>Username</label>
+ <description>Account username</description>
+ </parameter>
+ <parameter name="password" type="text" required="true">
+ <label>Password</label>
+ <description>Account password</description>
+ <context>password</context>
+ </parameter>
+ <parameter name="refreshInterval" type="integer" min="30" required="true" unit="s">
+ <label>Refresh Interval</label>
+ <description>Specifies the refresh interval in seconds</description>
+ <default>60</default>
+ </parameter>
+ </config-description>
+
+ <config-description uri="thing-type:generacmobilelink:generator">
+ <parameter name="generatorId" type="text" required="true">
+ <label>Generator ID</label>
+ <description>Generator ID</description>
+ </parameter>
+ </config-description>
+
+</config-description:config-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="generacmobilelink"
+ 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="account">
+ <label>MobileLink Account</label>
+ <description>MobileLink Cloud Account</description>
+ <config-description-ref uri="thing-type:generacmobilelink:account"/>
+ </bridge-type>
+
+ <thing-type id="generator">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+ <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="status" typeId="status"/>
+ <channel id="currentAlarmDescription" typeId="currentAlarmDescription"/>
+ <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"/>
+ </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>
+ <label>Status</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="currentAlarmDescription">
+ <item-type>String</item-type>
+ <label>Current Alarm Description</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="runHours">
+ <item-type>Number:Time</item-type>
+ <label>Number of Hours Run</label>
+ <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"/>
+ </channel-type>
+ <channel-type id="serviceStatus">
+ <item-type>Switch</item-type>
+ <label>Service Status</label>
+ <state readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
<module>org.openhab.binding.ftpupload</module>
<module>org.openhab.binding.gardena</module>
<module>org.openhab.binding.gce</module>
+ <module>org.openhab.binding.generacmobilelink</module>
<module>org.openhab.binding.goecharger</module>
<module>org.openhab.binding.globalcache</module>
<module>org.openhab.binding.gpstracker</module>