diff --git a/CODEOWNERS b/CODEOWNERS
index 96f873cd0c5..4f4cbc4e7a4 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -80,6 +80,7 @@
/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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 63d1f4424fe..33a19ae51f5 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -386,6 +386,11 @@
org.openhab.binding.gce
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.generacmobilelink
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.globalcache
diff --git a/bundles/org.openhab.binding.generacmobilelink/NOTICE b/bundles/org.openhab.binding.generacmobilelink/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/NOTICE
@@ -0,0 +1,13 @@
+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
diff --git a/bundles/org.openhab.binding.generacmobilelink/README.md b/bundles/org.openhab.binding.generacmobilelink/README.md
new file mode 100644
index 00000000000..adb7ccd1e40
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/README.md
@@ -0,0 +1,95 @@
+# 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
+ }
+}
+```
diff --git a/bundles/org.openhab.binding.generacmobilelink/pom.xml b/bundles/org.openhab.binding.generacmobilelink/pom.xml
new file mode 100644
index 00000000000..ca299fd5197
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.generacmobilelink
+
+ openHAB Add-ons :: Bundles :: GeneracMobileLink Binding
+
+
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml b/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml
new file mode 100644
index 00000000000..71ed25e54b3
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml
@@ -0,0 +1,23 @@
+
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.generacmobilelink/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java
new file mode 100644
index 00000000000..e27e02bf3db
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java
@@ -0,0 +1,29 @@
+/**
+ * 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");
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkAccountConfiguration.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkAccountConfiguration.java
new file mode 100644
index 00000000000..def1179060b
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkAccountConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java
new file mode 100644
index 00000000000..c0de87f4437
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java
@@ -0,0 +1,64 @@
+/**
+ * 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 SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set.of(THING_TYPE_GENERATOR);
+
+ public GeneracMobileLinkDiscoveryService() {
+ super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 0);
+ }
+
+ @Override
+ public Set 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java
new file mode 100644
index 00000000000..ee8abfe1c57
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java
@@ -0,0 +1,23 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java
new file mode 100644
index 00000000000..299954dda68
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java
@@ -0,0 +1,54 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java
new file mode 100644
index 00000000000..645517d0611
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java
@@ -0,0 +1,25 @@
+/**
+ * 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 {
+
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java
new file mode 100644
index 00000000000..a47deccebbb
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java
@@ -0,0 +1,31 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java
new file mode 100644
index 00000000000..b8382c8e336
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java
@@ -0,0 +1,23 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java
new file mode 100644
index 00000000000..292e1929809
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java
@@ -0,0 +1,95 @@
+/**
+ * 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 SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
+ THING_TYPE_GENERATOR);
+ private final Map> 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();
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java
new file mode 100644
index 00000000000..c19b5a25206
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java
@@ -0,0 +1,251 @@
+/**
+ * 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 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 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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java
new file mode 100644
index 00000000000..b12d1684a62
--- /dev/null
+++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java
@@ -0,0 +1,99 @@
+/**
+ * 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