[ring] Initial Contribution (#18668)
* Initial contribution Co-authored-by: Ben Rosenblum <rosenblumb@gmail.com> Co-authored-by: morph166955 <53797132+morph166955@users.noreply.github.com> Signed-off-by: Paul Smedley <paul@smedley.id.au>pull/18768/head
parent
8eb0e0f913
commit
b5c1b3a9d8
|
@ -329,6 +329,7 @@
|
|||
/bundles/org.openhab.binding.resol/ @ramack
|
||||
/bundles/org.openhab.binding.revogi/ @openhab/add-ons-maintainers
|
||||
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
|
||||
/bundles/org.openhab.binding.ring/ @morph166955
|
||||
/bundles/org.openhab.binding.rme/ @kgoderis
|
||||
/bundles/org.openhab.binding.robonect/ @reyem
|
||||
/bundles/org.openhab.binding.roku/ @mlobstein
|
||||
|
|
|
@ -1621,6 +1621,11 @@
|
|||
<artifactId>org.openhab.binding.rfxcom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.ring</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.rme</artifactId>
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,98 @@
|
|||
# Ring Binding
|
||||
|
||||
This is an experimental binding to the Ring.com API.
|
||||
It currently supports a Ring account and is able to discover Ring Video Doorbells, Stick Up Cameras, Chimes, and other devices.
|
||||
They need to be registered in the Ring account before they will be detected.
|
||||
|
||||
It currently does *not* support live video streaming, but you can view recorded videos, if this service is enabled in the Ring account.
|
||||
|
||||
## Supported Things
|
||||
|
||||
The binding currently supports Ring Video Doorbell, Stick Up Cameras, Chimes, and others.
|
||||
*Other* is identified as any of the non-traditional types such as the intercom.
|
||||
|
||||
## Discovery
|
||||
|
||||
Auto-discovery is supported by this binding.
|
||||
After (manually) adding a Ring Account bridge, registered doorbells and chimes will be auto discovered.
|
||||
|
||||
## Account Configuration
|
||||
|
||||
Account configuration is necessary.
|
||||
The easiest way to do this is from the UI.
|
||||
Just add a new thing, select the Ring binding, then Ring Account Binding Thing, and enter username and password.
|
||||
Optionally, you can also specify a unique hardware ID and refresh interval for how often to check ring.com for events.
|
||||
If hardware ID is not specified, the MAC address of the system running OpenHAB is used.
|
||||
| Parameter | Description | Default |
|
||||
|---------------------|---------------------------------------------------------------------|-------------|
|
||||
| username | The user name you use to subscribe to the Ring services. | N/A |
|
||||
| password | The password you use to subscribe to the Ring services. | N/A |
|
||||
| twofactorCode | 2 factor authentication code (Where enabled) | N/A |
|
||||
| hardwareId | A unique hardware id | N/A |
|
||||
| refreshInterval | Refresh interval | 5 |
|
||||
| videoStoragePath | Video Download Path | N/A |
|
||||
| videoRetentionCount | Number of videos to keep | 10 |
|
||||
|
||||
## Channels
|
||||
|
||||
### Control group (all things):
|
||||
|
||||
| Channel Type ID | Item Type | Description |
|
||||
|-----------------|-----------|---------------------------------------|
|
||||
| enabled | Switch | Enable polling of this device/account |
|
||||
|
||||
### Events group (Ring Account Binding Thing only):
|
||||
|
||||
| Channel Type ID | Item Type | Description |
|
||||
|--------------------|-----------|----------------------------------------------------------------------------------------------|
|
||||
| url | String | The URL to a recorded video (only when subscribed on ring.com) |
|
||||
| createdAt | DateTime | The date and time the event was created |
|
||||
| kind | String | The kind of event, usually 'motion' or 'ding' |
|
||||
| doorbotId | String | The internal id of the doorbot that generated the currently selected event |
|
||||
| doorbotDescription | String | The description of the doorbot that generated the currently selected event (e.g. Front Door) |
|
||||
|
||||
### Device Status (Video Doorbell Binding Thing, Stickup Cam Binding Thing, Other Binding Thing only):
|
||||
|
||||
| Channel Type ID | Item Type | Description |
|
||||
|------------------|-----------|---------------------|
|
||||
| battery | Number | Battery level in % |
|
||||
|
||||
## Full Example
|
||||
|
||||
NOTE 1: Replace <ring_device_id> with a valid ring device ID when manually configuring.
|
||||
The easiest way to currently get that is to define the account bridge and pull the device ID from the last event channel.
|
||||
|
||||
NOTE 2: Text configuration for the Things ONLY works if you DO NOT have 2 factor authentication enabled.
|
||||
If you are using 2 factor authentication, Things MUST be set up through Main UI.
|
||||
|
||||
ring.things:
|
||||
|
||||
```java
|
||||
ring:account:ringAccount "Ring Account" [ username="user@domain.com", password="XXXXXXX", hardwareId="AA-BB-CC-DD-EE-FF", refreshInterval=5 ]
|
||||
ring:doorbell:<ring_device_id> "Ring Doorbell" [ refreshInterval=5, offOffset=0 ]
|
||||
ring:chime:<ring_device_id> "Ring Chime" [ refreshInterval=5, offOffset=0 ]
|
||||
ring:stickupcam:<ring_device_id> "Ring Stickup Camera" [ refreshInterval=5, offOffset=0 ]
|
||||
ring:other:<ring_device_id> "Ring Other Device" [ refreshInterval=5, offOffset=0 ]
|
||||
```
|
||||
|
||||
ring.items:
|
||||
|
||||
```java
|
||||
Switch RingAccountEnabled "Ring Account Polling Enabled" { channel="ring:account:ringAccount:control#enabled" }
|
||||
String RingEventVideoURL "Ring Event URL" { channel="ring:account:ringAccount:event#url" }
|
||||
DateTime RingEventCreated "Ring Event Created" { channel="ring:account:ringAccount:event#createdAt" }
|
||||
String RingEventKind "Ring Event Kind" { channel="ring:account:ringAccount:event#kind" }
|
||||
String RingEventDeviceID "Ring Device ID" { channel="ring:account:ringAccount:event#doorbotId" }
|
||||
String RingEventDeviceDescription "Ring Device Description" { channel="ring:account:ringAccount:event#doorbotDescription" }
|
||||
|
||||
Switch RingDoorbellEnabled "Ring Doorbell Polling Enabled" { channel="ring:doorbell:<ring_device_id>:control#enabled" }
|
||||
Number RingDoorbellBattery "Ring Doorbell Battery [%s]%" { channel="ring:doorbell:<ring_device_id>:status#battery"}
|
||||
|
||||
Switch RingChimeEnabled "Ring Chime Polling Enabled" { channel="ring:chime:<ring_device_id>:control#enabled" }
|
||||
|
||||
Switch RingStickupEnabled "Ring Stickup Polling Enabled" { channel="ring:stickupcam:<ring_device_id>:control#enabled" }
|
||||
Number RingStickupBattery "Ring Stickup Battery [%s]%" { channel="ring:stickupcam:<ring_device_id>:status#battery"}
|
||||
|
||||
Switch RingOtherEnabled "Ring Other Polling Enabled" { channel="ring:other:<ring_device_id>:control#enabled" }
|
||||
Number RingOtherBattery "Ring Other Battery [%s]%" { channel="ring:other:<ring_device_id>:status#battery"}
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.ring</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Ring Binding</name>
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.ring-${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-ring" description="Ring Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ring/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link RingBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RingBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "ring";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
|
||||
public static final ThingTypeUID THING_TYPE_DOORBELL = new ThingTypeUID(BINDING_ID, "doorbell");
|
||||
public static final ThingTypeUID THING_TYPE_CHIME = new ThingTypeUID(BINDING_ID, "chime");
|
||||
public static final ThingTypeUID THING_TYPE_STICKUPCAM = new ThingTypeUID(BINDING_ID, "stickupcam");
|
||||
public static final ThingTypeUID THING_TYPE_OTHERDEVICE = new ThingTypeUID(BINDING_ID, "otherdevice");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_CONTROL_STATUS = "control#status";
|
||||
public static final String CHANNEL_CONTROL_ENABLED = "control#enabled";
|
||||
|
||||
public static final String CHANNEL_STATUS_BATTERY = "status#battery";
|
||||
|
||||
public static final String CHANNEL_EVENT_URL = "event#url";
|
||||
public static final String CHANNEL_EVENT_CREATED_AT = "event#createdAt";
|
||||
public static final String CHANNEL_EVENT_KIND = "event#kind";
|
||||
public static final String CHANNEL_EVENT_DOORBOT_ID = "event#doorbotId";
|
||||
public static final String CHANNEL_EVENT_DOORBOT_DESCRIPTION = "event#doorbotDescription";
|
||||
|
||||
public static final String SERVLET_VIDEO_PATH = "/ring/video";
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_DOORBELL,
|
||||
THING_TYPE_CHIME, THING_TYPE_STICKUPCAM, THING_TYPE_OTHERDEVICE);
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link AbstractRingHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractRingHandler extends BaseThingHandler {
|
||||
|
||||
public Gson gson;
|
||||
|
||||
// Current status
|
||||
protected OnOffType status = OnOffType.OFF;
|
||||
protected OnOffType enabled = OnOffType.ON;
|
||||
protected final Logger logger = LoggerFactory.getLogger(AbstractRingHandler.class);
|
||||
|
||||
// Scheduler
|
||||
protected @Nullable ScheduledFuture<?> refreshJob;
|
||||
|
||||
protected AbstractRingHandler(Thing thing, Gson gson) {
|
||||
super(thing);
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing AbstractRingHandler");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the state of channels that may have changed by (re-)initialization.
|
||||
*/
|
||||
protected abstract void refreshState();
|
||||
|
||||
/**
|
||||
* Called every minute
|
||||
*/
|
||||
protected abstract void minuteTick();
|
||||
|
||||
private void refresh() {
|
||||
try {
|
||||
minuteTick();
|
||||
} catch (final Exception e) {
|
||||
logger.debug("AbstractHandler - Exception occurred during execution of startAutomaticRefresh(): {}",
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check every 60 seconds if one of the alarm times is reached.
|
||||
*/
|
||||
protected void startAutomaticRefresh(final int refreshInterval) {
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
refreshState();
|
||||
}
|
||||
|
||||
protected void stopAutomaticRefresh() {
|
||||
ScheduledFuture<?> job = refreshJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
refreshJob = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose off the refreshJob nicely.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopAutomaticRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
final String id = getThing().getUID().getId();
|
||||
final RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
try {
|
||||
registry.removeRingDevice(id);
|
||||
} catch (final DeviceNotFoundException e) {
|
||||
logger.debug("Exception occurred during execution of handleRemoval(): {}", e.getMessage(), e);
|
||||
} finally {
|
||||
updateStatus(ThingStatus.REMOVED);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AccountConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Ben Rosenblum - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AccountConfiguration {
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
public String hardwareId = "";
|
||||
public String twofactorCode = "";
|
||||
public int videoRetentionCount;
|
||||
public String videoStoragePath = "";
|
||||
public int refreshInterval;
|
||||
}
|
|
@ -0,0 +1,610 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.RestClient;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.RingVideoServlet;
|
||||
import org.openhab.binding.ring.internal.data.Profile;
|
||||
import org.openhab.binding.ring.internal.data.RingDevices;
|
||||
import org.openhab.binding.ring.internal.data.RingEventTO;
|
||||
import org.openhab.binding.ring.internal.errors.AuthenticationException;
|
||||
import org.openhab.binding.ring.internal.errors.DuplicateIdException;
|
||||
import org.openhab.binding.ring.internal.utils.RingUtils;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link AccountHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Peter Mietlowski - oAuth upgrade and additional maintenance
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class AccountHandler extends BaseBridgeHandler implements RingAccount {
|
||||
|
||||
private @Nullable ScheduledFuture<?> jobTokenRefresh = null;
|
||||
private @Nullable ScheduledFuture<?> eventRefresh = null;
|
||||
private @Nullable Runnable runnableVideo = null;
|
||||
private @Nullable RingVideoServlet ringVideoServlet;
|
||||
private final HttpService httpService;
|
||||
private final String thingId;
|
||||
|
||||
// Current status
|
||||
protected OnOffType status = OnOffType.OFF;
|
||||
protected OnOffType enabled = OnOffType.ON;
|
||||
protected final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
|
||||
|
||||
// Scheduler
|
||||
protected @Nullable ScheduledFuture<?> refreshJob;
|
||||
|
||||
/**
|
||||
* The user profile retrieved when authenticating.
|
||||
*/
|
||||
private Profile userProfile = new Profile();
|
||||
/**
|
||||
* The registry.
|
||||
*/
|
||||
private final RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
/**
|
||||
* The RestClient is used to connect to the Ring Account.
|
||||
*/
|
||||
private RestClient restClient = new RestClient();
|
||||
/**
|
||||
* The list with events.
|
||||
*/
|
||||
private List<RingEventTO> lastEvents = List.of();
|
||||
/**
|
||||
* The index to the current event.
|
||||
*/
|
||||
private int eventIndex = 0;
|
||||
|
||||
private @Nullable ExecutorService videoExecutorService;
|
||||
|
||||
/*
|
||||
* The number of video files to keep when auto-downloading
|
||||
*/
|
||||
private int videoRetentionCount;
|
||||
|
||||
/*
|
||||
* The path of where to save video files
|
||||
*/
|
||||
private String videoStoragePath = "";
|
||||
|
||||
private final NetworkAddressService networkAddressService;
|
||||
|
||||
private final int httpPort;
|
||||
|
||||
public AccountHandler(Bridge bridge, NetworkAddressService networkAddressService, HttpService httpService,
|
||||
int httpPort) {
|
||||
super(bridge);
|
||||
this.httpPort = httpPort;
|
||||
this.networkAddressService = networkAddressService;
|
||||
this.httpService = httpService;
|
||||
this.videoExecutorService = Executors.newCachedThreadPool();
|
||||
this.thingId = this.getThing().getUID().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
boolean eventListOk = lastEvents.size() > eventIndex;
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_EVENT_URL:
|
||||
if (eventListOk) {
|
||||
String videoFile = restClient.downloadEventVideo(lastEvents.get(eventIndex), userProfile,
|
||||
videoStoragePath, videoRetentionCount);
|
||||
String localIP = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
|
||||
if (videoFile.endsWith(".mp4")) {
|
||||
updateState(channelUID,
|
||||
new StringType("http://" + localIP + ":" + httpPort + "/ring/video/" + videoFile));
|
||||
} else {
|
||||
updateState(channelUID, new StringType(videoFile));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_EVENT_CREATED_AT:
|
||||
if (eventListOk) {
|
||||
updateState(channelUID, lastEvents.get(eventIndex).getCreatedAt());
|
||||
}
|
||||
break;
|
||||
case CHANNEL_EVENT_KIND:
|
||||
if (eventListOk) {
|
||||
updateState(channelUID, new StringType(lastEvents.get(eventIndex).kind));
|
||||
}
|
||||
break;
|
||||
case CHANNEL_EVENT_DOORBOT_ID:
|
||||
if (eventListOk) {
|
||||
updateState(channelUID, new StringType(lastEvents.get(eventIndex).doorbot.id));
|
||||
}
|
||||
break;
|
||||
case CHANNEL_EVENT_DOORBOT_DESCRIPTION:
|
||||
if (eventListOk) {
|
||||
updateState(channelUID, new StringType(lastEvents.get(eventIndex).doorbot.description));
|
||||
}
|
||||
break;
|
||||
case CHANNEL_CONTROL_ENABLED:
|
||||
updateState(channelUID, enabled);
|
||||
break;
|
||||
default:
|
||||
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
|
||||
break;
|
||||
}
|
||||
} else if (command instanceof OnOffType xcommand) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CONTROL_ENABLED:
|
||||
if (!enabled.equals(xcommand)) {
|
||||
enabled = xcommand;
|
||||
updateState(channelUID, enabled);
|
||||
if (enabled.equals(OnOffType.ON)) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = ConfigParser.valueAsOrElse(config.get("refreshInterval"),
|
||||
BigDecimal.class, BigDecimal.valueOf(500)).intValue();
|
||||
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
} else {
|
||||
stopAutomaticRefresh();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
try {
|
||||
minuteTick();
|
||||
} catch (final Exception e) {
|
||||
logger.debug("AbstractHandler - Exception occurred during execution of startAutomaticRefresh(): {}",
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void startAutomaticRefresh(final int refreshInterval) {
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected void stopAutomaticRefresh() {
|
||||
ScheduledFuture<?> job = refreshJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
refreshJob = null;
|
||||
}
|
||||
|
||||
private void saveRefreshTokenToFile(String refreshToken) {
|
||||
String folderName = OpenHAB.getUserDataFolder() + "/ring";
|
||||
String thingId = this.thingId;
|
||||
File folder = new File(folderName);
|
||||
String fileName = folderName + "/ring." + thingId + ".refreshToken";
|
||||
|
||||
if (!folder.exists()) {
|
||||
logger.debug("Creating directory {}", folderName);
|
||||
folder.mkdirs();
|
||||
}
|
||||
try {
|
||||
Files.write(Paths.get(fileName), refreshToken.getBytes());
|
||||
} catch (IOException ex) {
|
||||
logger.debug("IOException when writing refreshToken to file {}", ex.getMessage());
|
||||
}
|
||||
logger.debug("saveRefreshTokenToFile Successful {}", RingUtils.sanitizeData(refreshToken));
|
||||
}
|
||||
|
||||
private String getRefreshTokenFromFile() {
|
||||
String refreshToken = "";
|
||||
String folderName = OpenHAB.getUserDataFolder() + "/ring";
|
||||
String thingId = this.thingId;
|
||||
String fileName = folderName + "/ring." + thingId + ".refreshToken";
|
||||
File file = new File(fileName);
|
||||
if (!file.exists()) {
|
||||
return refreshToken;
|
||||
}
|
||||
try {
|
||||
final byte[] contents = Files.readAllBytes(Paths.get(fileName));
|
||||
refreshToken = new String(contents);
|
||||
} catch (IOException ex) {
|
||||
logger.debug("IOException when reading refreshToken from file {}", ex.getMessage());
|
||||
}
|
||||
logger.debug("getRefreshTokenFromFile successful {}", RingUtils.sanitizeData(refreshToken));
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public void doLogin(String username, String password, String twofactorCode) {
|
||||
logger.debug("doLogin U:{} P:{} 2:{}", RingUtils.sanitizeData(username), RingUtils.sanitizeData(password),
|
||||
RingUtils.sanitizeData(twofactorCode));
|
||||
String hardwareId = getHardwareId();
|
||||
String refreshToken = getRefreshTokenFromFile();
|
||||
logger.debug("doLogin H:{} RT:{}", hardwareId, RingUtils.sanitizeData(refreshToken));
|
||||
try {
|
||||
userProfile = restClient.getAuthenticatedProfile(username, password, refreshToken, twofactorCode,
|
||||
hardwareId);
|
||||
saveRefreshTokenToFile(userProfile.getRefreshToken());
|
||||
} catch (AuthenticationException ex) {
|
||||
logger.debug("AuthenticationException when initializing Ring Account handler{}", ex.getMessage());
|
||||
String message = ex.getMessage();
|
||||
if ((message != null) && message.startsWith("Two factor")) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("Invalid response from api.ring.com when initializing Ring Account handler{}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Invalid response from api.ring.com");
|
||||
}
|
||||
logger.debug("doLogin RT: {}", getRefreshTokenFromFile());
|
||||
try {
|
||||
refreshRegistry();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (DuplicateIdException dup) {
|
||||
logger.debug("Ring device with duplicate id detected, ignoring device");
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AuthenticationException ae) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"AuthenticationException response from ring.com");
|
||||
logger.debug("RestClient reported AuthenticationException in finally block: {}", ae.getMessage());
|
||||
} catch (JsonParseException pe1) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"JsonParseException response from ring.com");
|
||||
logger.debug("RestClient reported JsonParseException in finally block: {}", pe1.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String getHardwareId() {
|
||||
AccountConfiguration config = getConfigAs(AccountConfiguration.class);
|
||||
String hardwareId = config.hardwareId;
|
||||
logger.debug("getHardwareId H:{}", hardwareId);
|
||||
Configuration updatedConfiguration = getThing().getConfiguration();
|
||||
try {
|
||||
if (hardwareId.isEmpty()) {
|
||||
hardwareId = getLocalMAC();
|
||||
if (("".equals(hardwareId)) || hardwareId.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Hardware ID missing, check thing config");
|
||||
return hardwareId;
|
||||
}
|
||||
logger.debug("getHardwareId getLocalMac H:{}", hardwareId);
|
||||
// write hardwareId to thing config
|
||||
config.hardwareId = hardwareId;
|
||||
updatedConfiguration.put("hardwareId", config.hardwareId);
|
||||
updateConfiguration(updatedConfiguration);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("getHardwareId failed to get local mac address {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Initialization failed: " + e.getMessage());
|
||||
}
|
||||
return hardwareId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Ring Account handler");
|
||||
|
||||
AccountConfiguration config = getConfigAs(AccountConfiguration.class);
|
||||
int refreshInterval = config.refreshInterval;
|
||||
String username = config.username;
|
||||
String password = config.password;
|
||||
String hardwareId = getHardwareId();
|
||||
String refreshToken = getRefreshTokenFromFile();
|
||||
|
||||
String twofactorCode = config.twofactorCode;
|
||||
videoRetentionCount = config.videoRetentionCount;
|
||||
videoStoragePath = !config.videoStoragePath.isEmpty() ? config.videoStoragePath
|
||||
: OpenHAB.getConfigFolder() + "/html/ring/video";
|
||||
|
||||
logger.debug("AccountHandler - initialize - VSP: {} OH: {}", config.videoStoragePath,
|
||||
OpenHAB.getConfigFolder());
|
||||
|
||||
restClient = new RestClient();
|
||||
|
||||
if ((!refreshToken.isEmpty()) || !(username.isEmpty() && password.isEmpty())) {
|
||||
try {
|
||||
Configuration updatedConfiguration = getThing().getConfiguration();
|
||||
|
||||
logger.debug("Logging in with refresh token: {}", RingUtils.sanitizeData(refreshToken));
|
||||
userProfile = restClient.getAuthenticatedProfile(username, password, refreshToken, twofactorCode,
|
||||
hardwareId);
|
||||
saveRefreshTokenToFile(userProfile.getRefreshToken());
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Retrieving device list");
|
||||
config.twofactorCode = "";
|
||||
updatedConfiguration.put("twofactorCode", config.twofactorCode);
|
||||
updateConfiguration(updatedConfiguration);
|
||||
|
||||
if (this.ringVideoServlet == null) {
|
||||
this.ringVideoServlet = new RingVideoServlet(httpService, videoStoragePath);
|
||||
}
|
||||
|
||||
// Note: When initialization can NOT be done set the status with more details for further
|
||||
// analysis. See also class ThingStatusDetail for all available status details.
|
||||
// Add a description to give user information to understand why thing does not work
|
||||
// as expected. E.g.
|
||||
// updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
// "Can not access device as username and/or password are invalid");
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
startSessionRefresh(refreshInterval);
|
||||
} catch (AuthenticationException ex) {
|
||||
logger.debug("AuthenticationException when initializing Ring Account handler {}", ex.getMessage());
|
||||
String message = ex.getMessage();
|
||||
if ((message != null) && message.startsWith("Two factor")) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("Invalid response from api.ring.com when initializing Ring Account handler {}",
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Invalid response from api.ring.com");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"Please login via CLI or by updating the Thing properties");
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshRegistry() throws JsonParseException, AuthenticationException, DuplicateIdException {
|
||||
logger.debug("AccountHandler - refreshRegistry");
|
||||
RingDevices ringDevices = restClient.getRingDevices(userProfile, this);
|
||||
registry.addRingDevices(ringDevices.getRingDevices());
|
||||
}
|
||||
|
||||
protected void minuteTick() {
|
||||
try {
|
||||
// Init the devices
|
||||
refreshRegistry();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AuthenticationException | JsonParseException e) {
|
||||
logger.debug(
|
||||
"AuthenticationException in AccountHandler.minuteTick() when trying refreshRegistry, attempting to reconnect {}",
|
||||
e.getMessage());
|
||||
AccountConfiguration config = getConfigAs(AccountConfiguration.class);
|
||||
String username = config.username;
|
||||
String password = config.password;
|
||||
String hardwareId = getHardwareId();
|
||||
String refreshToken = getRefreshTokenFromFile();
|
||||
if ((!refreshToken.isEmpty()) || !(username.isEmpty() && password.isEmpty())) {
|
||||
try {
|
||||
userProfile = restClient.getAuthenticatedProfile(username, password, refreshToken, "", hardwareId);
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Retrieving device list");
|
||||
} catch (AuthenticationException ex) {
|
||||
logger.debug("RestClient reported AuthenticationException trying getAuthenticatedProfile: {}",
|
||||
ex.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Invalid credentials");
|
||||
} catch (JsonParseException e1) {
|
||||
logger.debug("RestClient reported JsonParseException trying getAuthenticatedProfile: {}",
|
||||
e1.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Invalid response from api.ring.com");
|
||||
} finally {
|
||||
try {
|
||||
refreshRegistry();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (DuplicateIdException ignored) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AuthenticationException ae) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"AuthenticationException response from ring.com");
|
||||
logger.debug("RestClient reported AuthenticationException in finally block: {}",
|
||||
ae.getMessage());
|
||||
} catch (JsonParseException pe1) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"JsonParseException response from ring.com");
|
||||
logger.debug("RestClient reported JsonParseException in finally block: {}", pe1.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (DuplicateIdException ignored) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void getVideo(RingEventTO event) {
|
||||
logger.debug("AccountHandler - getVideo - Event id: {}", event.id);
|
||||
logger.debug("AccountHandler - getVideo - VSP: {}", videoStoragePath);
|
||||
String videoFile = restClient.downloadEventVideo(event, userProfile, videoStoragePath, videoRetentionCount);
|
||||
String localIP = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
|
||||
if (videoFile.endsWith(".mp4")) {
|
||||
updateState(new ChannelUID(thing.getUID(), CHANNEL_EVENT_URL),
|
||||
new StringType("http://" + localIP + ":" + httpPort + "/ring/video/" + videoFile));
|
||||
} else {
|
||||
updateState(new ChannelUID(thing.getUID(), CHANNEL_EVENT_URL), new StringType(videoFile));
|
||||
}
|
||||
}
|
||||
|
||||
protected void eventTick() {
|
||||
try {
|
||||
long id = lastEvents.isEmpty() ? 0 : lastEvents.get(0).id;
|
||||
lastEvents = restClient.getHistory(userProfile, 1);
|
||||
if (!lastEvents.isEmpty()) {
|
||||
logger.debug("AccountHandler - eventTick - Event id: {} lastEvents: {}", id,
|
||||
lastEvents.get(0).id == id);
|
||||
if (lastEvents.get(0).id != id) {
|
||||
logger.debug("AccountHandler - eventTick - New Event {}", lastEvents.get(0).id);
|
||||
updateState(new ChannelUID(thing.getUID(), CHANNEL_EVENT_CREATED_AT),
|
||||
lastEvents.get(0).getCreatedAt());
|
||||
updateState(new ChannelUID(thing.getUID(), CHANNEL_EVENT_KIND),
|
||||
new StringType(lastEvents.get(0).kind));
|
||||
updateState(new ChannelUID(thing.getUID(), CHANNEL_EVENT_DOORBOT_ID),
|
||||
new StringType(lastEvents.get(0).doorbot.id));
|
||||
updateState(new ChannelUID(thing.getUID(), CHANNEL_EVENT_DOORBOT_DESCRIPTION),
|
||||
new StringType(lastEvents.get(0).doorbot.description));
|
||||
runnableVideo = () -> getVideo(lastEvents.get(0));
|
||||
ExecutorService service = videoExecutorService;
|
||||
if (service != null) {
|
||||
service.submit(runnableVideo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("AccountHandler - eventTick - lastEvents null");
|
||||
}
|
||||
} catch (AuthenticationException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"AuthenticationException response from ring.com");
|
||||
logger.debug(
|
||||
"RestClient reported AuthenticationExceptionfrom api.ring.com when retrying refreshRegistry for the second time: {}",
|
||||
ex.getMessage());
|
||||
} catch (JsonParseException ignored) {
|
||||
logger.debug(
|
||||
"RestClient reported JsonParseException api.ring.com when retrying refreshRegistry for the second time: {}",
|
||||
ignored.getMessage());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshToken() {
|
||||
try {
|
||||
refreshRegistry();
|
||||
Configuration config = getThing().getConfiguration();
|
||||
String hardwareId = (String) config.get("hardwareId");
|
||||
userProfile = restClient.getAuthenticatedProfile("", "", userProfile.getRefreshToken(), "", hardwareId);
|
||||
} catch (AuthenticationException | DuplicateIdException e) {
|
||||
logger.debug(
|
||||
"AccountHandler - startSessionRefresh - Exception occurred during execution of refreshRegistry(): {}",
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshEvent() {
|
||||
try {
|
||||
eventTick();
|
||||
} catch (final Exception e) {
|
||||
logger.debug(
|
||||
"AccountHandler - startSessionRefresh - Exception occurred during execution of eventTick(): {}",
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the profile every 20 minutes
|
||||
*/
|
||||
protected void startSessionRefresh(int refreshInterval) {
|
||||
logger.debug("startSessionRefresh {}", refreshInterval);
|
||||
jobTokenRefresh = scheduler.scheduleWithFixedDelay(this::refreshToken, 90, 600, TimeUnit.SECONDS);
|
||||
eventRefresh = scheduler.scheduleWithFixedDelay(this::refreshEvent, refreshInterval, refreshInterval,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected void stopSessionRefresh() {
|
||||
ScheduledFuture<?> job = jobTokenRefresh;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
jobTokenRefresh = null;
|
||||
|
||||
job = eventRefresh;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
eventRefresh = null;
|
||||
}
|
||||
|
||||
String getLocalMAC() throws IOException {
|
||||
// get local ip from OH system settings
|
||||
String localIP = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
if ((localIP == null) || (localIP.isBlank())) {
|
||||
logger.debug("No local IP selected in openHAB system configuration");
|
||||
return "";
|
||||
}
|
||||
|
||||
// get MAC address
|
||||
InetAddress ip = InetAddress.getByName(localIP);
|
||||
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
||||
if (network != null) {
|
||||
byte[] mac = network.getHardwareAddress();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));
|
||||
}
|
||||
String localMAC = sb.toString();
|
||||
logger.debug("Local IP address='{}', local MAC address = '{}'", localIP, localMAC);
|
||||
return localMAC;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable RestClient getRestClient() {
|
||||
return restClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile getProfile() {
|
||||
return userProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThingId() {
|
||||
return thingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the refreshJob nicely.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopSessionRefresh();
|
||||
stopAutomaticRefresh();
|
||||
ExecutorService service = this.videoExecutorService;
|
||||
if (service != null) {
|
||||
service.shutdownNow();
|
||||
}
|
||||
this.videoExecutorService = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.data.Chime;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.binding.ring.internal.errors.IllegalDeviceClassException;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
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.types.Command;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The handler for a Ring Chime.
|
||||
*
|
||||
* @author Ben Rosenblum - Initial contribution
|
||||
*
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class ChimeHandler extends RingDeviceHandler {
|
||||
public ChimeHandler(Thing thing, Gson gson) {
|
||||
super(thing, gson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Chime handler");
|
||||
super.initialize();
|
||||
|
||||
RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
String id = getThing().getUID().getId();
|
||||
if (registry.isInitialized()) {
|
||||
try {
|
||||
linkDevice(id, Chime.class);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (DeviceNotFoundException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' not found");
|
||||
} catch (IllegalDeviceClassException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' of wrong type");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"Waiting for RingAccount to initialize");
|
||||
}
|
||||
|
||||
// Note: When initialization can NOT be done set the status with more details for further
|
||||
// analysis. See also class ThingStatusDetail for all available status details.
|
||||
// Add a description to give user information to understand why thing does not work
|
||||
// as expected. E.g.
|
||||
// updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
// "Can not access device as username and/or password are invalid");
|
||||
if (this.refreshJob == null) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = ConfigParser
|
||||
.valueAsOrElse(config.get("refreshInterval"), BigDecimal.class, BigDecimal.valueOf(500)).intValue();
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshState() {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void minuteTick() {
|
||||
logger.debug("ChimeHandler - minuteTick - device {}", getThing().getUID().getId());
|
||||
if (device == null) {
|
||||
initialize();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.CHANNEL_STATUS_BATTERY;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.data.Doorbell;
|
||||
import org.openhab.binding.ring.internal.data.RingDeviceTO;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.binding.ring.internal.errors.IllegalDeviceClassException;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
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.types.Command;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The handler for a Ring Video Doorbell.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DoorbellHandler extends RingDeviceHandler {
|
||||
private int lastBattery = -1;
|
||||
|
||||
public DoorbellHandler(Thing thing, Gson gson) {
|
||||
super(thing, gson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Doorbell handler");
|
||||
super.initialize();
|
||||
|
||||
RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
String id = getThing().getUID().getId();
|
||||
if (registry.isInitialized()) {
|
||||
try {
|
||||
linkDevice(id, Doorbell.class);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (DeviceNotFoundException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' not found");
|
||||
} catch (IllegalDeviceClassException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' of wrong type");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"Waiting for RingAccount to initialize");
|
||||
}
|
||||
|
||||
// Note: When initialization can NOT be done set the status with more details for further
|
||||
// analysis. See also class ThingStatusDetail for all available status details.
|
||||
// Add a description to give user information to understand why thing does not work
|
||||
// as expected. E.g.
|
||||
// updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
// "Can not access device as username and/or password are invalid");
|
||||
if (this.refreshJob == null) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = ConfigParser
|
||||
.valueAsOrElse(config.get("refreshInterval"), BigDecimal.class, BigDecimal.valueOf(500)).intValue();
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshState() {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void minuteTick() {
|
||||
logger.debug("DoorbellHandler - minuteTick - device {}", getThing().getUID().getId());
|
||||
if (device == null) {
|
||||
initialize();
|
||||
}
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if ((deviceTO != null) && (deviceTO.health.batteryPercentage != lastBattery)) {
|
||||
logger.debug("Battery Level: {}", deviceTO.health.batteryPercentage);
|
||||
ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_STATUS_BATTERY);
|
||||
updateState(channelUID, new DecimalType(deviceTO.health.batteryPercentage));
|
||||
lastBattery = deviceTO.health.batteryPercentage;
|
||||
} else if (deviceTO != null) {
|
||||
logger.debug("Battery Level Unchanged for {} - {} vs {}", getThing().getUID().getId(),
|
||||
deviceTO.health.batteryPercentage, lastBattery);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.CHANNEL_STATUS_BATTERY;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.data.OtherDevice;
|
||||
import org.openhab.binding.ring.internal.data.RingDeviceTO;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.binding.ring.internal.errors.IllegalDeviceClassException;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
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.types.Command;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The handler for a Ring Other Device.
|
||||
*
|
||||
* @author Ben Rosenblum - Initial Contribution
|
||||
*
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OtherDeviceHandler extends RingDeviceHandler {
|
||||
private int lastBattery = -1;
|
||||
|
||||
public OtherDeviceHandler(Thing thing, Gson gson) {
|
||||
super(thing, gson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Other Device handler");
|
||||
super.initialize();
|
||||
|
||||
RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
String id = getThing().getUID().getId();
|
||||
if (registry.isInitialized()) {
|
||||
try {
|
||||
linkDevice(id, OtherDevice.class);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (DeviceNotFoundException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' not found");
|
||||
} catch (IllegalDeviceClassException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' of wrong type");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"Waiting for RingAccount to initialize");
|
||||
}
|
||||
|
||||
// Note: When initialization can NOT be done set the status with more details for further
|
||||
// analysis. See also class ThingStatusDetail for all available status details.
|
||||
// Add a description to give user information to understand why thing does not work
|
||||
// as expected. E.g.
|
||||
// updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
// "Can not access device as username and/or password are invalid");
|
||||
if (this.refreshJob == null) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = ConfigParser
|
||||
.valueAsOrElse(config.get("refreshInterval"), BigDecimal.class, BigDecimal.valueOf(500)).intValue();
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshState() {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void minuteTick() {
|
||||
logger.debug("OtherDeviceHandler - minuteTick - device {}", getThing().getUID().getId());
|
||||
if (device == null) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if ((deviceTO != null) && (deviceTO.health.batteryPercentage != lastBattery)) {
|
||||
logger.debug("Battery Level: {}", deviceTO.health.batteryPercentage);
|
||||
ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_STATUS_BATTERY);
|
||||
updateState(channelUID, new DecimalType(deviceTO.health.batteryPercentage));
|
||||
lastBattery = deviceTO.health.batteryPercentage;
|
||||
} else if (deviceTO != null) {
|
||||
logger.debug("Battery Level Unchanged for {} - {} vs {}", getThing().getUID().getId(),
|
||||
deviceTO.health.batteryPercentage, lastBattery);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.data.RingDevice;
|
||||
import org.openhab.binding.ring.internal.data.RingDeviceTO;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.binding.ring.internal.errors.IllegalDeviceClassException;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link RingDeviceHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public abstract class RingDeviceHandler extends AbstractRingHandler {
|
||||
|
||||
/**
|
||||
* The RingDevice instance linked to this thing.
|
||||
*/
|
||||
protected @Nullable RingDevice device;
|
||||
|
||||
public RingDeviceHandler(Thing thing, Gson gson) {
|
||||
super(thing, gson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link the device, and update the device with the status CONFIGURED.
|
||||
*
|
||||
* @param id the device id
|
||||
* @param deviceClass the expected class
|
||||
* @throws DeviceNotFoundException when device is not found in the RingDeviceRegistry.
|
||||
* @throws IllegalDeviceClassException when the registered device is of the wrong type.
|
||||
*/
|
||||
protected void linkDevice(String id, Class<?> deviceClass)
|
||||
throws DeviceNotFoundException, IllegalDeviceClassException {
|
||||
device = RingDeviceRegistry.getInstance().getRingDevice(id);
|
||||
if (device != null) {
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if (deviceClass.equals(device.getClass())) {
|
||||
device.setRegistrationStatus(RingDeviceRegistry.Status.CONFIGURED);
|
||||
device.setRingDeviceHandler(this);
|
||||
if (deviceTO != null) {
|
||||
thing.setProperty("Description", deviceTO.description);
|
||||
thing.setProperty("Kind", deviceTO.kind);
|
||||
thing.setProperty("Device ID", deviceTO.deviceId);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalDeviceClassException("Class '" + deviceClass.getName() + "' expected but '"
|
||||
+ device.getClass().getName() + "' found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle generic commands, common to all Ring Devices.
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof Number || command instanceof RefreshType || command instanceof IncreaseDecreaseType
|
||||
|| command instanceof UpDownType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CONTROL_ENABLED:
|
||||
updateState(channelUID, enabled);
|
||||
break;
|
||||
case CHANNEL_STATUS_BATTERY:
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if (deviceTO != null) {
|
||||
updateState(channelUID, new DecimalType(deviceTO.health.batteryPercentage));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
|
||||
break;
|
||||
}
|
||||
refreshState();
|
||||
} else if (command instanceof OnOffType xcommand) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CONTROL_ENABLED:
|
||||
if (!enabled.equals(xcommand)) {
|
||||
enabled = xcommand;
|
||||
updateState(channelUID, enabled);
|
||||
if (enabled.equals(OnOffType.ON)) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = (int) config.get("refreshInterval");
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
} else {
|
||||
stopAutomaticRefresh();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.handler;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.CHANNEL_STATUS_BATTERY;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.data.RingDeviceTO;
|
||||
import org.openhab.binding.ring.internal.data.Stickupcam;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.binding.ring.internal.errors.IllegalDeviceClassException;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
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.types.Command;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The handler for a Ring Video Stickup Cam.
|
||||
*
|
||||
* @author Chris Milbert - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StickupcamHandler extends RingDeviceHandler {
|
||||
private int lastBattery = -1;
|
||||
|
||||
public StickupcamHandler(Thing thing, Gson gson) {
|
||||
super(thing, gson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Stickupcam handler");
|
||||
super.initialize();
|
||||
|
||||
RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
String id = getThing().getUID().getId();
|
||||
if (registry.isInitialized()) {
|
||||
try {
|
||||
linkDevice(id, Stickupcam.class);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (DeviceNotFoundException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' not found");
|
||||
} catch (IllegalDeviceClassException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Device with id '" + id + "' of wrong type");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"Waiting for RingAccount to initialize");
|
||||
}
|
||||
|
||||
// Note: When initialization can NOT be done set the status with more details for further
|
||||
// analysis. See also class ThingStatusDetail for all available status details.
|
||||
// Add a description to give user information to understand why thing does not work
|
||||
// as expected. E.g.
|
||||
// updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
// "Can not access device as username and/or password are invalid");
|
||||
if (this.refreshJob == null) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = ConfigParser
|
||||
.valueAsOrElse(config.get("refreshInterval"), BigDecimal.class, BigDecimal.valueOf(500)).intValue();
|
||||
startAutomaticRefresh(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshState() {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void minuteTick() {
|
||||
logger.debug("StickupcamHandler - minuteTick - device {}", getThing().getUID().getId());
|
||||
if (device == null) {
|
||||
initialize();
|
||||
}
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if ((deviceTO != null) && (deviceTO.health.batteryPercentage != lastBattery)) {
|
||||
logger.debug("Battery Level: {}", deviceTO.battery);
|
||||
ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_STATUS_BATTERY);
|
||||
updateState(channelUID, new DecimalType(deviceTO.health.batteryPercentage));
|
||||
lastBattery = deviceTO.health.batteryPercentage;
|
||||
} else if (deviceTO != null) {
|
||||
logger.debug("Battery Level Unchanged for {} - {} vs {}", getThing().getUID().getId(),
|
||||
deviceTO.health.batteryPercentage, lastBattery);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class ApiConstants {
|
||||
public static final int API_VERSION = 11;
|
||||
|
||||
// API resources
|
||||
public static final String API_USER_AGENT = "OpenHAB Ring Binding";
|
||||
public static final String API_OAUTH_ENDPOINT = "https://oauth.ring.com/oauth/token";
|
||||
public static final String API_BASE = "https://api.ring.com";
|
||||
public static final String URL_SESSION = API_BASE + "/clients_api/session";
|
||||
public static final String URL_DEVICES = API_BASE + "/clients_api/ring_devices";
|
||||
public static final String URL_HISTORY = API_BASE + "/clients_api/doorbots/history";
|
||||
public static final String URL_RECORDING_START = API_BASE + "/clients_api/dings/";
|
||||
public static final String URL_RECORDING_END = "/share/play?disable_redirect=true";
|
||||
public static final String URL_DOORBELLS = API_BASE + "/clients_api/doorbots";
|
||||
public static final String URL_CHIMES = API_BASE + "/clients_api/chimes";
|
||||
|
||||
public static final String URL_RECORDING = "/clients_api/dings/{0}/recording";
|
||||
|
||||
// JSON data names for ring devices
|
||||
public static final String DEVICES_DOORBOTS = "doorbots";
|
||||
public static final String DEVICES_CHIMES = "chimes";
|
||||
public static final String DEVICES_STICKUP_CAMS = "stickup_cams";
|
||||
public static final String DEVICES_OTHERDEVICE = "other";
|
||||
}
|
|
@ -0,0 +1,631 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.data.DataFactory;
|
||||
import org.openhab.binding.ring.internal.data.ParamBuilder;
|
||||
import org.openhab.binding.ring.internal.data.Profile;
|
||||
import org.openhab.binding.ring.internal.data.RingDevices;
|
||||
import org.openhab.binding.ring.internal.data.RingEventTO;
|
||||
import org.openhab.binding.ring.internal.errors.AuthenticationException;
|
||||
import org.openhab.binding.ring.internal.utils.RingUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Pete Mietlowski - Updated authentication routines
|
||||
* @author Chris Milbert - Stickupcam contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class RestClient {
|
||||
public static final Type RING_EVENT_LIST_TYPE = new TypeToken<List<RingEventTO>>() {
|
||||
}.getType();
|
||||
private static final int CONNECTION_TIMEOUT = 12000;
|
||||
private final Logger logger = LoggerFactory.getLogger(RestClient.class);
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private static final String METHOD_POST = "POST";
|
||||
private static final String METHOD_GET = "GET";
|
||||
|
||||
// The factory to create data elements
|
||||
// private DataElementFactory factory;
|
||||
|
||||
/**
|
||||
* Create a new client with the given server and port address.
|
||||
*/
|
||||
public RestClient() {
|
||||
logger.debug("Creating Ring client for API version {} on endPoint {}", ApiConstants.API_VERSION,
|
||||
ApiConstants.API_BASE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to given url
|
||||
*
|
||||
* @param resourceUrl
|
||||
* @param data
|
||||
* @param oauthToken
|
||||
* @return the servers response
|
||||
* @throws AuthenticationException
|
||||
*
|
||||
*/
|
||||
|
||||
private String postRequest(String resourceUrl, String data, String oauthToken) throws AuthenticationException {
|
||||
String result = "";
|
||||
logger.trace("RestClient - postRequest: {} - {} - {}", resourceUrl, data, oauthToken);
|
||||
try {
|
||||
byte[] postData = data.getBytes(StandardCharsets.UTF_8);
|
||||
StringBuilder output = new StringBuilder();
|
||||
URL url = new URI(resourceUrl).toURL();
|
||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
||||
conn.setDoInput(true);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestProperty("User-Agent", ApiConstants.API_USER_AGENT);
|
||||
conn.setRequestProperty("Authorization", "Bearer " + oauthToken);
|
||||
conn.setHostnameVerifier((hostname, session) -> true);
|
||||
// SSL setting
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, new TrustManager[] { new javax.net.ssl.X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate @Nullable [] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
} }, null);
|
||||
conn.setSSLSocketFactory(context.getSocketFactory());
|
||||
conn.setRequestMethod(METHOD_POST);
|
||||
|
||||
conn.setRequestProperty("X-API-LANG", "en");
|
||||
conn.setRequestProperty("Content-length", "gzip, deflate");
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
|
||||
OutputStream out = conn.getOutputStream();
|
||||
out.write(postData);
|
||||
logger.debug("RestApi postRequest: {}, response code: {}, message {}.", resourceUrl, conn.getResponseCode(),
|
||||
conn.getResponseMessage());
|
||||
switch (conn.getResponseCode()) {
|
||||
case 200, 201:
|
||||
break;
|
||||
case 400, 401:
|
||||
throw new AuthenticationException("Invalid username or password");
|
||||
case 429:
|
||||
throw new AuthenticationException("Account ratelimited");
|
||||
default:
|
||||
logger.warn("Unhandled http response code: {}", conn.getResponseCode());
|
||||
throw new AuthenticationException("Failed : HTTP error code : " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
output.append(line);
|
||||
}
|
||||
conn.disconnect();
|
||||
result = output.toString();
|
||||
logger.trace("RestApi postRequest response: {}.", result);
|
||||
} catch (IOException | KeyManagementException | NoSuchAlgorithmException | URISyntaxException ex) {
|
||||
logger.error("RestApi error in postRequest!", ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from given url
|
||||
*
|
||||
* @param resourceUrl
|
||||
* @param profile
|
||||
* @return the servers response
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
private String getRequest(String resourceUrl, Profile profile) throws AuthenticationException {
|
||||
String result = "";
|
||||
logger.trace("RestClient - getRequest: {}", resourceUrl);
|
||||
try {
|
||||
StringBuilder output = new StringBuilder();
|
||||
URL url = new URI(resourceUrl).toURL();
|
||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
||||
conn.setDoInput(true);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestProperty("User-Agent", ApiConstants.API_USER_AGENT);
|
||||
conn.setHostnameVerifier((hostname, session) -> true);
|
||||
// SSL setting
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, new TrustManager[] { new javax.net.ssl.X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate @Nullable [] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
} }, null);
|
||||
conn.setSSLSocketFactory(context.getSocketFactory());
|
||||
conn.setRequestMethod(METHOD_GET);
|
||||
|
||||
conn.setRequestProperty("cache-control", "no-cache");
|
||||
conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
|
||||
conn.setRequestProperty("authorization", "Bearer " + profile.getAccessToken());
|
||||
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(12000);
|
||||
|
||||
switch (conn.getResponseCode()) {
|
||||
case 200, 201:
|
||||
break;
|
||||
case 400, 401:
|
||||
// break;
|
||||
throw new AuthenticationException("Invalid request");
|
||||
case 429:
|
||||
throw new AuthenticationException("Account ratelimited");
|
||||
default:
|
||||
logger.warn("Unhandled http response code: {}", conn.getResponseCode());
|
||||
throw new AuthenticationException("Failed : HTTP error code : " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
if (conn.getResponseCode() != 200) {
|
||||
logger.debug("RestApi getRequest: {}, response code: {}, message {}.", resourceUrl,
|
||||
conn.getResponseCode(), conn.getResponseMessage());
|
||||
}
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
output.append(line);
|
||||
}
|
||||
conn.disconnect();
|
||||
result = output.toString();
|
||||
if (!result.startsWith("[{\"id\"")) { // Ignore ding results
|
||||
logger.trace("RestApi getRequest response: {}.", result);
|
||||
}
|
||||
} catch (IOException | KeyManagementException | NoSuchAlgorithmException | URISyntaxException ex) {
|
||||
logger.debug("RestApi error in getRequest!", ex);
|
||||
// ex.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a (new) authenticated profile.
|
||||
*
|
||||
* @param username the username of the Ring account.
|
||||
* @param password the password for the Ring account.
|
||||
* @param hardwareId a hardware ID (must be unique for every piece of hardware used).
|
||||
* @return a Profile instance with available data stored in it.
|
||||
* @throws AuthenticationException
|
||||
* @throws JsonParseException
|
||||
*/
|
||||
public Profile getAuthenticatedProfile(String username, String password, String refreshToken, String twofactorCode,
|
||||
String hardwareId) throws AuthenticationException, JsonParseException {
|
||||
String refToken = refreshToken;
|
||||
|
||||
logger.debug("RestClient - getAuthenticatedProfile U:{} - P:{} - R:{} - 2:{} - H:{}",
|
||||
RingUtils.sanitizeData(username), RingUtils.sanitizeData(password),
|
||||
RingUtils.sanitizeData(refreshToken), RingUtils.sanitizeData(twofactorCode),
|
||||
RingUtils.sanitizeData(hardwareId));
|
||||
|
||||
if (!twofactorCode.isBlank()) {
|
||||
logger.debug("RestClient - getAuthenticatedProfile - valid 2fa - run getAuthCode");
|
||||
refToken = getAuthCode(twofactorCode, username, password, hardwareId);
|
||||
}
|
||||
|
||||
JsonObject oauthToken = getOauthToken(username, password, refToken);
|
||||
String jsonResult = postRequest(ApiConstants.URL_SESSION, DataFactory.getSessionParams(hardwareId),
|
||||
oauthToken.get("access_token").getAsString());
|
||||
|
||||
JsonObject obj = JsonParser.parseString(jsonResult).getAsJsonObject();
|
||||
return new Profile((JsonObject) obj.get("profile"), oauthToken.get("refresh_token").getAsString(),
|
||||
oauthToken.get("access_token").getAsString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a (new) oAuth token.
|
||||
*
|
||||
* @param username the username of the Ring account.
|
||||
* @param password the password for the Ring account.
|
||||
* @return a JsonObject with the available data stored in it (access_token, refresh_token)
|
||||
* @throws AuthenticationException
|
||||
* @throws JsonParseException
|
||||
*/
|
||||
private JsonObject getOauthToken(String username, String password, String refreshToken)
|
||||
throws AuthenticationException, JsonParseException {
|
||||
logger.debug("RestClient - getOauthToken {} - {} - {}", RingUtils.sanitizeData(username),
|
||||
RingUtils.sanitizeData(password), RingUtils.sanitizeData(refreshToken));
|
||||
|
||||
String result = null;
|
||||
JsonObject oauthToken = new JsonObject();
|
||||
String resourceUrl = ApiConstants.API_OAUTH_ENDPOINT;
|
||||
try {
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
|
||||
map.put("client_id", "ring_official_android");
|
||||
map.put("scope", "client");
|
||||
if (refreshToken.isBlank()) {
|
||||
logger.debug("RestClient - getOauthToken - refreshToken null or empty {}",
|
||||
RingUtils.sanitizeData(refreshToken));
|
||||
map.put("grant_type", "password");
|
||||
map.put("username", username);
|
||||
map.put("password", password);
|
||||
} else {
|
||||
logger.debug("RestClient - getOauthToken - refreshToken NOT null or empty {}",
|
||||
RingUtils.sanitizeData(refreshToken));
|
||||
map.put("grant_type", "refresh_token");
|
||||
map.put("refresh_token", refreshToken);
|
||||
}
|
||||
URL url = new URI(resourceUrl).toURL();
|
||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
||||
conn.setDoInput(true);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestProperty("User-Agent", ApiConstants.API_USER_AGENT);
|
||||
conn.setHostnameVerifier((hostname, session) -> true);
|
||||
// SSL setting
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, new TrustManager[] { new javax.net.ssl.X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate @Nullable [] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
} }, null);
|
||||
conn.setSSLSocketFactory(context.getSocketFactory());
|
||||
conn.setRequestMethod(METHOD_POST);
|
||||
|
||||
conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded; charset: UTF-8");
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
|
||||
StringJoiner sj = new StringJoiner("&");
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
sj.add(URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + URLEncoder.encode(entry.getValue(), "UTF-8"));
|
||||
}
|
||||
byte[] out = sj.toString().getBytes(StandardCharsets.UTF_8);
|
||||
int length = out.length;
|
||||
|
||||
conn.setFixedLengthStreamingMode(length);
|
||||
conn.connect();
|
||||
OutputStream os = conn.getOutputStream();
|
||||
os.write(out);
|
||||
|
||||
logger.debug("RestClient getOauthToken: {}, response code: {}, message {}.", resourceUrl,
|
||||
conn.getResponseCode(), conn.getResponseMessage());
|
||||
|
||||
switch (conn.getResponseCode()) {
|
||||
case 200, 201:
|
||||
break;
|
||||
case 400:
|
||||
throw new AuthenticationException("Two factor authentication enabled, enter code");
|
||||
case 412:
|
||||
if (conn.getResponseMessage().startsWith("Precondition")) {
|
||||
throw new AuthenticationException("Two factor authentication enabled, enter code");
|
||||
}
|
||||
case 401:
|
||||
throw new AuthenticationException("Invalid username or password.");
|
||||
case 429:
|
||||
throw new AuthenticationException("Account ratelimited");
|
||||
default:
|
||||
logger.warn("Unhandled http response code: {}", conn.getResponseCode());
|
||||
throw new AuthenticationException("Failed : HTTP error code : " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
result = readFullyAsString(conn.getInputStream(), "UTF-8");
|
||||
conn.disconnect();
|
||||
|
||||
oauthToken = JsonParser.parseString(result).getAsJsonObject();
|
||||
logger.debug("RestClient response: {}.", RingUtils.sanitizeData(result));
|
||||
} catch (IOException | KeyManagementException | NoSuchAlgorithmException | URISyntaxException ex) {
|
||||
logger.error("RestApi: Error in getOauthToken!", ex);
|
||||
}
|
||||
return oauthToken;
|
||||
}
|
||||
|
||||
public String readFullyAsString(InputStream inputStream, String encoding) throws IOException {
|
||||
return readFully(inputStream).toString(encoding);
|
||||
}
|
||||
|
||||
private ByteArrayOutputStream readFully(InputStream inputStream) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int length = 0;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, length);
|
||||
}
|
||||
return baos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to given url
|
||||
*
|
||||
* @param authCode
|
||||
* @param username
|
||||
* @param password
|
||||
* @param hardwareId
|
||||
* @return the servers response
|
||||
* @throws AuthenticationException
|
||||
*
|
||||
*/
|
||||
|
||||
private String getAuthCode(String authCode, String username, String password, String hardwareId)
|
||||
throws AuthenticationException {
|
||||
logger.debug("RestClient - getAuthCode A:{} - U:{} - P:{} - H:{}", RingUtils.sanitizeData(authCode),
|
||||
RingUtils.sanitizeData(username), RingUtils.sanitizeData(password), RingUtils.sanitizeData(hardwareId));
|
||||
|
||||
String result = "";
|
||||
|
||||
String resourceUrl = ApiConstants.API_OAUTH_ENDPOINT;
|
||||
try {
|
||||
ParamBuilder pb = new ParamBuilder(false);
|
||||
pb.add("client_id", "ring_official_android");
|
||||
pb.add("scope", "client");
|
||||
pb.add("grant_type", "password");
|
||||
pb.add("password", password);
|
||||
pb.add("username", username);
|
||||
|
||||
URL url = new URI(resourceUrl).toURL();
|
||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
||||
conn.setDoInput(true);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestProperty("X-API-LANG", "en");
|
||||
conn.setRequestProperty("Content-length", "gzip, deflate");
|
||||
conn.setRequestProperty("2fa-support", "true");
|
||||
conn.setRequestProperty("2fa-code", authCode);
|
||||
conn.setRequestProperty("hardware_id", hardwareId);
|
||||
conn.setRequestProperty("User-Agent", ApiConstants.API_USER_AGENT);
|
||||
conn.setHostnameVerifier((hostname, session) -> true);
|
||||
// SSL setting
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, new TrustManager[] { new javax.net.ssl.X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate @Nullable [] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
} }, null);
|
||||
|
||||
conn.setSSLSocketFactory(context.getSocketFactory());
|
||||
conn.setRequestMethod(METHOD_POST);
|
||||
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
|
||||
byte[] out = pb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
conn.connect();
|
||||
OutputStream os = conn.getOutputStream();
|
||||
os.write(out);
|
||||
|
||||
logger.info("RestApi getAuthCode: {}, response code: {}, message {}.", resourceUrl, conn.getResponseCode(),
|
||||
conn.getResponseMessage());
|
||||
|
||||
switch (conn.getResponseCode()) {
|
||||
case 200, 201:
|
||||
break;
|
||||
case 400:
|
||||
throw new AuthenticationException("2 factor enabled, enter code");
|
||||
case 412:
|
||||
if (conn.getResponseMessage().startsWith("Verification Code")) {
|
||||
throw new AuthenticationException("2 factor enabled, enter code");
|
||||
}
|
||||
case 401:
|
||||
throw new AuthenticationException("Invalid username or password.");
|
||||
case 429:
|
||||
throw new AuthenticationException("Account ratelimited");
|
||||
default:
|
||||
logger.warn("Unhandled http response code: {}", conn.getResponseCode());
|
||||
throw new AuthenticationException("Failed : HTTP error code : " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
result = readFullyAsString(conn.getInputStream(), "UTF-8");
|
||||
conn.disconnect();
|
||||
|
||||
JsonObject refToken = JsonParser.parseString(result).getAsJsonObject();
|
||||
|
||||
result = refToken.get("refresh_token").getAsString();
|
||||
logger.debug("RestClient - getAuthCode response: {}.", RingUtils.sanitizeData(result));
|
||||
} catch (IOException | KeyManagementException | NoSuchAlgorithmException | URISyntaxException ex) {
|
||||
logger.error("Error getting auth code!", ex);
|
||||
} catch (JsonParseException e) {
|
||||
logger.error("Error parsing refToken", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the RingDevices instance, given the authenticated Profile.
|
||||
*
|
||||
* @param profile the Profile previously retrieved when authenticating.
|
||||
* @return the RingDevices instance filled with all available data.
|
||||
* @throws AuthenticationException when request is invalid.
|
||||
* @throws JsonParseException when response is invalid JSON.
|
||||
*/
|
||||
public RingDevices getRingDevices(Profile profile, RingAccount ringAccount)
|
||||
throws JsonParseException, AuthenticationException {
|
||||
logger.debug("RestClient - getRingDevices");
|
||||
String jsonResult = getRequest(ApiConstants.URL_DEVICES, profile);
|
||||
JsonObject obj = JsonParser.parseString(jsonResult).getAsJsonObject();
|
||||
return new RingDevices(obj, ringAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a List with the last recorded events, newest on top.
|
||||
*
|
||||
* @param profile the Profile previously retrieved when authenticating.
|
||||
* @param limit the maximum number of events.
|
||||
* @return
|
||||
* @throws AuthenticationException
|
||||
* @throws JsonParseException
|
||||
*/
|
||||
public synchronized List<RingEventTO> getHistory(Profile profile, int limit)
|
||||
throws AuthenticationException, JsonParseException {
|
||||
String jsonResult = getRequest(ApiConstants.URL_HISTORY + "?limit=" + limit, profile);
|
||||
if (!jsonResult.isBlank()) {
|
||||
return Objects.requireNonNull(gson.fromJson(jsonResult, RING_EVENT_LIST_TYPE));
|
||||
} else {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public String downloadEventVideo(RingEventTO event, Profile profile, String filePath, int retentionCount) {
|
||||
try {
|
||||
Path path = Paths.get(filePath);
|
||||
|
||||
try {
|
||||
Files.createDirectories(path.toAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
logger.error("RingVideo: Unable to create folder {}, cannot download.: {}", filePath, e.getMessage());
|
||||
return "";
|
||||
}
|
||||
if (retentionCount > 0 && Files.exists(path)) {
|
||||
// get FileSystem object
|
||||
FileSystem fs = path.getFileSystem();
|
||||
String sep = fs.getSeparator();
|
||||
String filename = event.doorbot.description.replace(" ", "") + "-" + event.kind + "-"
|
||||
+ event.getCreatedAt().toString().replace(":", "-") + ".mp4";
|
||||
String fullfilepath = filePath + (filePath.endsWith(sep) ? "" : sep) + filename;
|
||||
logger.info("fullfilepath = {}", fullfilepath);
|
||||
path = Paths.get(fullfilepath);
|
||||
boolean urlFound = false;
|
||||
if (Files.notExists(path)) {
|
||||
long eventId = event.id;
|
||||
StringBuilder vidUrl = new StringBuilder();
|
||||
vidUrl.append(ApiConstants.URL_RECORDING_START).append(eventId)
|
||||
.append(ApiConstants.URL_RECORDING_END);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
try {
|
||||
String jsonResult = getRequest(vidUrl.toString(), profile);
|
||||
JsonObject obj = JsonParser.parseString(jsonResult).getAsJsonObject();
|
||||
if (obj.get("url").getAsString().startsWith("http")) {
|
||||
URL url = new URI(obj.get("url").getAsString()).toURL();
|
||||
InputStream in = url.openStream();
|
||||
Files.copy(in, Paths.get(fullfilepath), StandardCopyOption.REPLACE_EXISTING);
|
||||
in.close();
|
||||
logger.info("fullfilepath.length() = {}", fullfilepath.length());
|
||||
if (!fullfilepath.isEmpty()) {
|
||||
urlFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (AuthenticationException | URISyntaxException e) {
|
||||
logger.debug("RingVideo: Error downloading file: {}", e.getMessage());
|
||||
} finally {
|
||||
Thread.sleep(15000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlFound) {
|
||||
File directory = new File(filePath);
|
||||
File[] logFiles = directory.listFiles();
|
||||
long oldestDate = Long.MAX_VALUE;
|
||||
File oldestFile = null;
|
||||
if (logFiles != null && logFiles.length > retentionCount) {
|
||||
// delete oldest files after there's more than the specified number of files
|
||||
for (File f : logFiles) {
|
||||
if (f.lastModified() < oldestDate) {
|
||||
oldestDate = f.lastModified();
|
||||
oldestFile = f;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestFile != null) {
|
||||
oldestFile.delete();
|
||||
}
|
||||
}
|
||||
return filename;
|
||||
} else {
|
||||
return "Video not available on ring.com";
|
||||
}
|
||||
} else if (retentionCount == 0) {
|
||||
return "videoRetentionCount = 0, Auto downloading disabled";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.warn("RingVideo: Unable to process request: {}", e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.data.Profile;
|
||||
|
||||
/**
|
||||
* The AccountHandler implements this interface to facilitate the
|
||||
* use of the common services.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface RingAccount {
|
||||
|
||||
/**
|
||||
* Get the linked REST client.
|
||||
*
|
||||
* @return the REST client.
|
||||
*/
|
||||
public @Nullable RestClient getRestClient();
|
||||
|
||||
/**
|
||||
* Get the linked user profile.
|
||||
*
|
||||
* @return the user profile.
|
||||
*/
|
||||
public @Nullable Profile getProfile();
|
||||
|
||||
/**
|
||||
* Get the Account Handler Thing ID
|
||||
* *
|
||||
*
|
||||
* @return the ring account thing id.
|
||||
*/
|
||||
public String getThingId();
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.data.RingDevice;
|
||||
import org.openhab.binding.ring.internal.data.RingDeviceTO;
|
||||
import org.openhab.binding.ring.internal.errors.DeviceNotFoundException;
|
||||
import org.openhab.binding.ring.internal.errors.DuplicateIdException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Singleton registry of found devices.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class RingDeviceRegistry {
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* static Singleton instance.
|
||||
*/
|
||||
private static final RingDeviceRegistry INSTANCE = new RingDeviceRegistry();
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
private final Logger logger = LoggerFactory.getLogger(RingDeviceRegistry.class);
|
||||
/**
|
||||
* Will be set after initialization.
|
||||
*/
|
||||
private boolean initialized;
|
||||
|
||||
/**
|
||||
* Key: device id.
|
||||
* Value: the RingDevice implementation object.
|
||||
*/
|
||||
private ConcurrentHashMap<String, RingDevice> devices = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Return a singleton instance of RingDeviceRegistry.
|
||||
*/
|
||||
public static RingDeviceRegistry getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new ring device.
|
||||
*/
|
||||
public void addRingDevice(RingDevice ringDevice) throws DuplicateIdException {
|
||||
RingDeviceTO deviceTO = gson.fromJson(ringDevice.getJsonObject(), RingDeviceTO.class);
|
||||
if (deviceTO != null) {
|
||||
if (devices.containsKey(deviceTO.id)) {
|
||||
throw new DuplicateIdException("Ring device with duplicate id " + deviceTO.id + " ignored");
|
||||
} else {
|
||||
ringDevice.setRegistrationStatus(Status.ADDED);
|
||||
devices.put(deviceTO.id, ringDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new ring device collection.
|
||||
*/
|
||||
public synchronized void addRingDevices(Collection<RingDevice> ringDevices) {
|
||||
for (RingDevice device : ringDevices) {
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if (deviceTO != null) {
|
||||
logger.debug("RingDeviceRegistry - addRingDevices - Trying: {}", deviceTO.id);
|
||||
try {
|
||||
addRingDevice(device);
|
||||
} catch (DuplicateIdException e) {
|
||||
logger.debug(
|
||||
"RingDeviceRegistry - addRingDevices - Ring device with duplicate id {} ignored. Updating Json.",
|
||||
deviceTO.id);
|
||||
devices.get(deviceTO.id).setJsonObject(device.getJsonObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true after the registry is filled with devices.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device registered with the given id.
|
||||
*
|
||||
* @param id the device id.
|
||||
* @return the RingDevice instance from the registry.
|
||||
* @throws DeviceNotFoundException
|
||||
*/
|
||||
public @Nullable RingDevice getRingDevice(String id) throws DeviceNotFoundException {
|
||||
if (devices.containsKey(id)) {
|
||||
return devices.get(id);
|
||||
} else {
|
||||
throw new DeviceNotFoundException("Device with id '" + id + "' not found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the device registered with the given id.
|
||||
*
|
||||
* @param id the device id.
|
||||
* @throws DeviceNotFoundException
|
||||
*/
|
||||
public void removeRingDevice(String id) throws DeviceNotFoundException {
|
||||
if (devices.containsKey(id)) {
|
||||
devices.remove(id);
|
||||
} else {
|
||||
throw new DeviceNotFoundException("Device with id '" + id + "' not found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection with RingDevices with the given status.
|
||||
*
|
||||
* @param filter the registration status to filter on.
|
||||
* @return the (possibly empty) collection.
|
||||
*/
|
||||
public Collection<RingDevice> getRingDevices(Status filterStatus) {
|
||||
return devices.values().stream().filter(d -> d.getRegistrationStatus().equals(filterStatus)).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the registration status.
|
||||
*
|
||||
* @param id the id of the RingDevice.
|
||||
* @param status the new registration status.
|
||||
*/
|
||||
public void setStatus(String id, Status status) {
|
||||
RingDevice result = devices.get(id);
|
||||
if (result != null) {
|
||||
result.setRegistrationStatus(status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry status of the device.
|
||||
*
|
||||
* @author Wim Vissers
|
||||
*
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* When first added to the registry, the status will be 'ADDED'.
|
||||
*/
|
||||
ADDED,
|
||||
/**
|
||||
* When reported to the system as discovered device. It will show up
|
||||
* in the inbox.
|
||||
*/
|
||||
DISCOVERED,
|
||||
/**
|
||||
* When a thing is created, the status will be configured.
|
||||
*/
|
||||
CONFIGURED;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.handler.AccountHandler;
|
||||
import org.openhab.binding.ring.handler.ChimeHandler;
|
||||
import org.openhab.binding.ring.handler.DoorbellHandler;
|
||||
import org.openhab.binding.ring.handler.OtherDeviceHandler;
|
||||
import org.openhab.binding.ring.handler.StickupcamHandler;
|
||||
import org.openhab.core.net.HttpServiceUtil;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link RingHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Chris Milbert - Stickupcam contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@Component(service = { ThingHandlerFactory.class,
|
||||
RingHandlerFactory.class }, immediate = true, configurationPid = "binding.ring")
|
||||
@NonNullByDefault
|
||||
public class RingHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(RingHandlerFactory.class);
|
||||
|
||||
private final NetworkAddressService networkAddressService;
|
||||
|
||||
private final HttpService httpService;
|
||||
private int httpPort;
|
||||
private @Nullable ComponentContext componentContext;
|
||||
|
||||
public final Gson gson = new Gson();
|
||||
|
||||
@Activate
|
||||
public RingHandlerFactory(@Reference NetworkAddressService networkAddressService,
|
||||
@Reference HttpService httpService, ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
httpPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
|
||||
if (httpPort == -1) {
|
||||
httpPort = 8080;
|
||||
}
|
||||
this.httpService = httpService;
|
||||
this.networkAddressService = networkAddressService;
|
||||
|
||||
logger.debug("Using OH HTTP port {}", httpPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(final Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
logger.info("createHandler thingType: {}", thingTypeUID);
|
||||
if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) {
|
||||
if (thing instanceof Bridge bridge) {
|
||||
return new AccountHandler(bridge, networkAddressService, httpService, httpPort);
|
||||
} else {
|
||||
logger.warn("Account Bridge configured as legacy Thing");
|
||||
return null;
|
||||
}
|
||||
} else if (thingTypeUID.equals(THING_TYPE_DOORBELL)) {
|
||||
return new DoorbellHandler(thing, gson);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_CHIME)) {
|
||||
return new ChimeHandler(thing, gson);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_STICKUPCAM)) {
|
||||
return new StickupcamHandler(thing, gson);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_OTHERDEVICE)) {
|
||||
return new OtherDeviceHandler(thing, gson);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Main OSGi service and HTTP servlet for Ring Video
|
||||
*
|
||||
* @author Peter Mietlowski (zolakk) - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@Component(service = HttpServlet.class)
|
||||
@NonNullByDefault
|
||||
public class RingVideoServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = -5592161948589682812L;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RingVideoServlet.class);
|
||||
|
||||
private String videoStoragePath = "";
|
||||
|
||||
public RingVideoServlet() {
|
||||
}
|
||||
|
||||
public RingVideoServlet(HttpService httpService, String videoStoragePath) {
|
||||
Path path = Paths.get(videoStoragePath);
|
||||
FileSystem fs = path.getFileSystem();
|
||||
String sep = fs.getSeparator();
|
||||
this.videoStoragePath = videoStoragePath + (videoStoragePath.endsWith(sep) ? "" : sep);
|
||||
try {
|
||||
httpService.registerServlet(SERVLET_VIDEO_PATH, this, null, httpService.createDefaultHttpContext());
|
||||
} catch (NamespaceException | ServletException e) {
|
||||
logger.warn("Register servlet fails", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
if (ipAddress == null) {
|
||||
ipAddress = request.getRemoteAddr();
|
||||
}
|
||||
String path = request.getRequestURI().substring(0, SERVLET_VIDEO_PATH.length());
|
||||
logger.trace("RingVideo: Request from {}:{}{} ({}:{}, {})", ipAddress, request.getRemotePort(), path,
|
||||
request.getRemoteHost(), request.getServerPort(), request.getProtocol());
|
||||
if (!request.getMethod().equalsIgnoreCase(HttpMethod.GET.toString())) {
|
||||
logger.warn("RingVideo: Unexpected method='{}'", request.getMethod());
|
||||
}
|
||||
if (!path.equalsIgnoreCase(SERVLET_VIDEO_PATH)) {
|
||||
logger.warn("RingVideo: Invalid request received - path = {}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
String uri = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
||||
|
||||
logger.debug("RingVideo: {} video '{}' requested", request.getMethod(), uri);
|
||||
|
||||
String filename = videoStoragePath + uri;
|
||||
File toBeCopied = new File(filename);
|
||||
String mimeType = URLConnection.guessContentTypeFromName(toBeCopied.getName());
|
||||
String contentDisposition = String.format("attachment; filename=%s", toBeCopied.getName());
|
||||
int fileSize = Long.valueOf(toBeCopied.length()).intValue();
|
||||
response.setHeader("Content-Disposition", contentDisposition);
|
||||
response.setContentLength(fileSize);
|
||||
response.setContentType(mimeType);
|
||||
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
try (OutputStream out = response.getOutputStream()) {
|
||||
Path videoPath = toBeCopied.toPath();
|
||||
Files.copy(videoPath, out);
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
// handle exception
|
||||
logger.error("RingVideo: Unable to process request: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.console;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.handler.AccountHandler;
|
||||
import org.openhab.core.io.console.Console;
|
||||
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
|
||||
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link RingCommandExtension} is responsible for handling console commands
|
||||
*
|
||||
* @author Ben Rosenblum - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
@Component(service = ConsoleCommandExtension.class)
|
||||
public class RingCommandExtension extends AbstractConsoleCommandExtension {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RingCommandExtension.class);
|
||||
private final ThingRegistry thingRegistry;
|
||||
|
||||
@Activate
|
||||
public RingCommandExtension(final @Reference ThingRegistry thingRegistry) {
|
||||
super("ring", "Interact with the Ring binding channels directly.");
|
||||
this.thingRegistry = thingRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String[] args, Console console) {
|
||||
if (args.length == 5 && "login".equals(args[1])) {
|
||||
logger.trace("Received Login Command: {} [username: {}, password: {}, 2FA: {}]", args[0], args[2], "***",
|
||||
"***");
|
||||
Thing thing = null;
|
||||
ThingUID thingUID = new ThingUID(args[0]);
|
||||
thing = thingRegistry.get(thingUID);
|
||||
ThingHandler thingHandler = null;
|
||||
AccountHandler handler = null;
|
||||
if (thing != null) {
|
||||
thingHandler = thing.getHandler();
|
||||
if (thingHandler instanceof AccountHandler) {
|
||||
handler = (AccountHandler) thingHandler;
|
||||
}
|
||||
}
|
||||
if (thing == null) {
|
||||
console.println("Bad thing uid '" + args[0] + "'");
|
||||
printUsage(console);
|
||||
} else if (thingHandler == null) {
|
||||
console.println("No handler initialized for the thing uid '" + args[0] + "'");
|
||||
printUsage(console);
|
||||
} else if (handler == null) {
|
||||
console.println("'" + args[0] + "' is not an Ring thing uid");
|
||||
printUsage(console);
|
||||
} else {
|
||||
logger.debug("Sending CLI login to handler {}", args[0]);
|
||||
handler.doLogin(args[2], args[3], args[4]);
|
||||
}
|
||||
} else {
|
||||
printUsage(console);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getUsages() {
|
||||
return List.of(buildCommandUsage("<thingUID> login <username> <password> <two factor auth>",
|
||||
"Send a login request to the RING API"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.handler.RingDeviceHandler;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Interface common to all Ring devices.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractRingDevice implements RingDevice {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractRingDevice.class);
|
||||
public final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* The JsonObject contains the data retrieved from the Ring API,
|
||||
* or the data to send to the API.
|
||||
*/
|
||||
protected JsonObject jsonObject = new JsonObject();
|
||||
/**
|
||||
* The registration status.
|
||||
*/
|
||||
private RingDeviceRegistry.Status registrationStatus = RingDeviceRegistry.Status.ADDED;
|
||||
/**
|
||||
* The linked Ring account.
|
||||
*/
|
||||
private final RingAccount ringAccount;
|
||||
/**
|
||||
* The linked RingDeviceHandler.
|
||||
*/
|
||||
private @Nullable RingDeviceHandler ringDeviceHandler;
|
||||
|
||||
protected AbstractRingDevice(JsonObject jsonObject, RingAccount ringAccount) {
|
||||
this.jsonObject = jsonObject;
|
||||
this.ringAccount = ringAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registration status.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public RingDeviceRegistry.Status getRegistrationStatus() {
|
||||
return registrationStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the registration status.
|
||||
*
|
||||
* @param status
|
||||
*/
|
||||
@Override
|
||||
public void setRegistrationStatus(RingDeviceRegistry.Status registrationStatus) {
|
||||
this.registrationStatus = registrationStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked Ring Device Handler.
|
||||
*
|
||||
* @return the handler.
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public RingDeviceHandler getRingDeviceHandler() {
|
||||
return ringDeviceHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the linked Ring Device Handler.
|
||||
*
|
||||
* @param ringDeviceHandler the handler.
|
||||
*/
|
||||
@Override
|
||||
public void setRingDeviceHandler(RingDeviceHandler ringDeviceHandler) {
|
||||
this.ringDeviceHandler = ringDeviceHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked Ring account.
|
||||
*
|
||||
* @return the account.
|
||||
*/
|
||||
@Override
|
||||
public RingAccount getRingAccount() {
|
||||
return ringAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setJsonObject(JsonObject jsonObject) {
|
||||
this.jsonObject = jsonObject;
|
||||
logger.trace("AbstractRingDevice - setJsonObject - Updated JSON: {}", this.jsonObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getJsonObject() {
|
||||
return this.jsonObject;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* @author Ben Rosenblum - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Chime extends AbstractRingDevice {
|
||||
|
||||
/**
|
||||
* Create Chime instance from JSON object.
|
||||
*
|
||||
* @param jsonChime the JSON Chime retrieved from the Ring API.
|
||||
* @param ringAccount the Ring Account in use
|
||||
*/
|
||||
public Chime(JsonObject jsonChime, RingAccount ringAccount) {
|
||||
super(jsonChime, ringAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DiscoveryResult object to identify the device as
|
||||
* discovered thing.
|
||||
*
|
||||
* @return the device as DiscoveryResult instance.
|
||||
*/
|
||||
@Override
|
||||
public DiscoveryResult getDiscoveryResult(RingDeviceTO deviceTO) {
|
||||
DiscoveryResult result = DiscoveryResultBuilder
|
||||
.create(new ThingUID("ring:chime:" + getRingAccount().getThingId() + ":" + deviceTO.id))
|
||||
.withLabel("Ring Chime - " + deviceTO.description).build();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.ApiConstants;
|
||||
|
||||
/**
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DataFactory {
|
||||
public static String getOauthData(String username, String password) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GET parameters for the session API resource.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSessionParams(String hardwareId) {
|
||||
ParamBuilder pb = new ParamBuilder(false);
|
||||
pb.add("device[os]", "android");
|
||||
pb.add("device[hardware_id]", hardwareId);
|
||||
pb.add("device[app_brand]", "ring");
|
||||
pb.add("device[metadata][device_model]", "VirtualBox");
|
||||
pb.add("device[metadata][resolution]", "600x800");
|
||||
pb.add("device[metadata][app_version]", "1.7.29");
|
||||
pb.add("device[metadata][app_installation_date]", "");
|
||||
pb.add("device[metadata][os_version]", "4.4.4");
|
||||
pb.add("device[metadata][manufacturer]", "innotek GmbH");
|
||||
pb.add("device[metadata][is_tablet]", "true");
|
||||
pb.add("device[metadata][linphone_initialized]", "true");
|
||||
pb.add("device[metadata][language]", "en");
|
||||
pb.add("api_version", "" + ApiConstants.API_VERSION);
|
||||
return pb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Doorbell extends AbstractRingDevice {
|
||||
|
||||
/**
|
||||
* Create Doorbell instance from JSON object.
|
||||
*
|
||||
* @param jsonDoorbell the JSON doorbell (doorbot) retrieved from the Ring API.
|
||||
* @param ringAccount the Ring Account in use
|
||||
*/
|
||||
public Doorbell(JsonObject jsonDoorbell, RingAccount ringAccount) {
|
||||
super(jsonDoorbell, ringAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DiscoveryResult object to identify the device as
|
||||
* discovered thing.
|
||||
*
|
||||
* @return the device as DiscoveryResult instance.
|
||||
*/
|
||||
@Override
|
||||
public DiscoveryResult getDiscoveryResult(RingDeviceTO deviceTO) {
|
||||
DiscoveryResult result = DiscoveryResultBuilder
|
||||
.create(new ThingUID("ring:doorbell:" + getRingAccount().getThingId() + ":" + deviceTO.id))
|
||||
.withLabel("Ring Video Doorbell - " + deviceTO.description).build();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Wim Vissers - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DoorbotTO {
|
||||
public String id = "";
|
||||
public String description = "";
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Feature {
|
||||
|
||||
REMOTE_LOGGING_FORMAT_STORING,
|
||||
REMOTE_LOGGING_LEVEL,
|
||||
SUBSCRIPTIONS_ENABLED,
|
||||
STICKUPCAM_SETUP_ENABLED,
|
||||
VOD_ENABLED,
|
||||
NW_ENABLED,
|
||||
NW_V2_ENABLED,
|
||||
NW_USER_ACTIVATED,
|
||||
RINGPLUS_ENABLED,
|
||||
LPD_ENABLED,
|
||||
REACTIVE_SNOOZING_ENABLED,
|
||||
PROACTIVE_SNOOZING_ENABLED,
|
||||
OWNER_PROACTIVE_SNOOZING_ENABLED,
|
||||
LIVE_VIEW_SETTINGS_ENABLED,
|
||||
DELETE_ALL_SETTINGS_ENABLED,
|
||||
POWER_CABLE_ENABLED,
|
||||
DEVICE_HEALTH_ALERTS_ENABLED,
|
||||
CHIME_PRO_ENABLED,
|
||||
MULTIPLE_CALLS_ENABLED,
|
||||
UJET_ENABLED,
|
||||
MULTIPLE_DELETE_ENABLED,
|
||||
DELETE_ALL_ENABLED,
|
||||
LPD_MOTION_ANNOUNCEMENT_ENABLED,
|
||||
STARRED_EVENTS_ENABLED,
|
||||
CHIME_DND_ENABLED,
|
||||
VIDEO_SEARCH_ENABLED,
|
||||
FLOODLIGHT_CAM_ENABLED,
|
||||
NW_LARGER_AREA_ENABLED,
|
||||
RING_CAM_BATTERY_ENABLED,
|
||||
ELITE_CAM_ENABLED,
|
||||
DOORBELL_V2_ENABLED,
|
||||
SPOTLIGHT_BATTERY_DASHBOARD_CONTROLS_ENABLED,
|
||||
BYPASS_ACCOUNT_VERIFICATION,
|
||||
LEGACY_CVR_RETENTION_ENABLED,
|
||||
NEW_DASHBOARD_ENABLED,
|
||||
RING_CAM_ENABLED,
|
||||
RING_SEARCH_ENABLED,
|
||||
RING_CAM_MOUNT_ENABLED,
|
||||
RING_ALARM_ENABLED,
|
||||
IN_APP_CALL_NOTIFICATIONS,
|
||||
RING_CASH_ELIGIBLE_ENABLED,
|
||||
NEW_RING_PLAYER_ENABLED,
|
||||
APP_ALERT_TONES_ENABLED,
|
||||
MOTION_SNOOZING_ENABLED;
|
||||
|
||||
/**
|
||||
* The enum is named according to the json names retrieved from
|
||||
* the Ring API, but in upper case.
|
||||
*
|
||||
* @return the json name.
|
||||
*/
|
||||
public String getJsonName() {
|
||||
return this.toString().toLowerCase();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* @author Ben Rosenblum - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OtherDevice extends AbstractRingDevice {
|
||||
|
||||
/**
|
||||
* Create OtherDevice instance from JSON object.
|
||||
*
|
||||
* @param jsonOtherDevice the JSON Other retrieved from the Ring API.
|
||||
* @param ringAccount the Ring Account in use
|
||||
*/
|
||||
public OtherDevice(JsonObject jsonOtherDevice, RingAccount ringAccount) {
|
||||
super(jsonOtherDevice, ringAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DiscoveryResult object to identify the device as
|
||||
* discovered thing.
|
||||
*
|
||||
* @return the device as DiscoveryResult instance.
|
||||
*/
|
||||
@Override
|
||||
public DiscoveryResult getDiscoveryResult(RingDeviceTO deviceTO) {
|
||||
DiscoveryResult result = DiscoveryResultBuilder
|
||||
.create(new ThingUID("ring:otherdevice:" + getRingAccount().getThingId() + ":" + deviceTO.id))
|
||||
.withLabel("Ring Other Device - " + deviceTO.description).build();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Builder for http request or post parameters.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class ParamBuilder {
|
||||
|
||||
/**
|
||||
* When true, URL encode parameter names and values properly.
|
||||
*/
|
||||
private boolean urlEncode;
|
||||
private static final String URL_ENCODING = "UTF-8";
|
||||
/**
|
||||
* The map used to store the parameters.
|
||||
*/
|
||||
private final Map<String, String> parameters;
|
||||
|
||||
/**
|
||||
* Create a new ParamBuilder. Specify if it should URL encode it.
|
||||
*
|
||||
* @param urlEncoded
|
||||
*/
|
||||
public ParamBuilder(boolean urlEncoded) {
|
||||
this.urlEncode = urlEncoded;
|
||||
this.parameters = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a name/value pair.
|
||||
*
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
public void add(String name, String value) {
|
||||
parameters.put(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle encoding.
|
||||
*
|
||||
* @param input the input String.
|
||||
* @return the (possibly encode) result.
|
||||
*/
|
||||
@Nullable
|
||||
private String encode(String input) {
|
||||
try {
|
||||
return urlEncode ? URLEncoder.encode(input, URL_ENCODING) : input;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Should not happen
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result string in the format param1=value1¶m2=value2, etc.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||
if (b.length() != 0) {
|
||||
b.append("&");
|
||||
}
|
||||
b.append(encode(entry.getKey())).append("=").append(encode(entry.getValue()));
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* {"profile":{
|
||||
* "id":4445516,
|
||||
* "email":"",
|
||||
* "first_name":null,
|
||||
* "last_name":null,
|
||||
* "phone_number":null,
|
||||
* "authentication_token":"CUBSmqFr9YE7cofLZKfy",
|
||||
* "features":
|
||||
* {
|
||||
* "remote_logging_format_storing":false,
|
||||
* "remote_logging_level":1,
|
||||
* "subscriptions_enabled":true,
|
||||
* "stickupcam_setup_enabled":true,
|
||||
* "vod_enabled":false,
|
||||
* "nw_enabled":true,
|
||||
* "nw_v2_enabled":true,
|
||||
* "nw_user_activated":false,
|
||||
* "ringplus_enabled":true,
|
||||
* "lpd_enabled":true,
|
||||
* "reactive_snoozing_enabled":false,
|
||||
* "proactive_snoozing_enabled":false,
|
||||
* "owner_proactive_snoozing_enabled":true,
|
||||
* "live_view_settings_enabled":true,
|
||||
* "delete_all_settings_enabled":false,
|
||||
* "power_cable_enabled":false,
|
||||
* "device_health_alerts_enabled":true,
|
||||
* "chime_pro_enabled":true,
|
||||
* "multiple_calls_enabled":true,
|
||||
* "ujet_enabled":true,
|
||||
* "multiple_delete_enabled":true,
|
||||
* "delete_all_enabled":true,
|
||||
* "lpd_motion_announcement_enabled":false,
|
||||
* "starred_events_enabled":true,
|
||||
* "chime_dnd_enabled":false,
|
||||
* "video_search_enabled":false,
|
||||
* "floodlight_cam_enabled":true,
|
||||
* "nw_larger_area_enabled":false,
|
||||
* "ring_cam_battery_enabled":true,
|
||||
* "elite_cam_enabled":true,
|
||||
* "doorbell_v2_enabled":true,
|
||||
* "spotlight_battery_dashboard_controls_enabled":false,
|
||||
* "bypass_account_verification":false,
|
||||
* "legacy_cvr_retention_enabled":false,
|
||||
* "new_dashboard_enabled":false,
|
||||
* "ring_cam_enabled":true,
|
||||
* "ring_search_enabled":false,
|
||||
* "ring_cam_mount_enabled":true,
|
||||
* "ring_alarm_enabled":false,
|
||||
* "in_app_call_notifications":true,
|
||||
* "ring_cash_eligible_enabled":true,
|
||||
* "new_ring_player_enabled":false,
|
||||
* "app_alert_tones_enabled":true,
|
||||
* "motion_snoozing_enabled":true
|
||||
* },
|
||||
* "hardware_id":"80940d0-7285-3366-8c64-6ea91491982b",
|
||||
* "explorer_program_terms":null,
|
||||
* "user_flow":"ring"
|
||||
* }}
|
||||
*
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Profile {
|
||||
private JsonObject jsonProfile = new JsonObject();
|
||||
private JsonObject jsonFeatures = new JsonObject();
|
||||
private String refreshToken = "";
|
||||
private String accessToken = "";
|
||||
|
||||
/**
|
||||
* Create Profile instance from JSON String.
|
||||
*
|
||||
* @param jsonProfile the JSON profile retrieved from the Ring API.
|
||||
* @param refreshToken needed for the refresh token so we aren't logging in every time.
|
||||
* Needed as a separate parameter because it's not part of the jsonProfile object.
|
||||
* @param accessToken needed for the access token so we aren't logging in every time.
|
||||
* Needed as a separate parameter because it's not part of the jsonProfile object.
|
||||
*/
|
||||
public Profile(JsonObject jsonProfile, String refreshToken, String accessToken) {
|
||||
this.jsonProfile = jsonProfile;
|
||||
this.jsonFeatures = (JsonObject) jsonProfile.get("features");
|
||||
this.refreshToken = refreshToken;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public Profile() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.handler.RingDeviceHandler;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Interface common to all Ring devices.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface RingDevice {
|
||||
|
||||
/**
|
||||
* Get the DiscoveryResult object to identify the device as
|
||||
* discovered thing.
|
||||
*
|
||||
* @return the device as DiscoveryResult instance.
|
||||
*/
|
||||
DiscoveryResult getDiscoveryResult(RingDeviceTO deviceTO);
|
||||
|
||||
/**
|
||||
* Get the registration status.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
RingDeviceRegistry.Status getRegistrationStatus();
|
||||
|
||||
/**
|
||||
* Set the registration status.
|
||||
*
|
||||
* @param registrationStatus
|
||||
*/
|
||||
void setRegistrationStatus(RingDeviceRegistry.Status registrationStatus);
|
||||
|
||||
/**
|
||||
* Get the linked Ring account.
|
||||
*
|
||||
* @return the account.
|
||||
*/
|
||||
RingAccount getRingAccount();
|
||||
|
||||
/**
|
||||
* Get the linked Ring Device Handler.
|
||||
*
|
||||
* @return the handler.
|
||||
*/
|
||||
@Nullable
|
||||
RingDeviceHandler getRingDeviceHandler();
|
||||
|
||||
/**
|
||||
* Set the linked Ring Device Handler.
|
||||
*
|
||||
* @param ringDeviceHandler the handler.
|
||||
*/
|
||||
void setRingDeviceHandler(RingDeviceHandler ringDeviceHandler);
|
||||
|
||||
void setJsonObject(JsonObject jsonObject);
|
||||
|
||||
JsonObject getJsonObject();
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Interface common to all Ring devices.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RingDeviceTO {
|
||||
|
||||
@SerializedName("id")
|
||||
public String id = "";
|
||||
|
||||
@SerializedName("kind")
|
||||
public String kind = "";
|
||||
|
||||
@SerializedName("description")
|
||||
public String description = "";
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId = "";
|
||||
|
||||
@SerializedName("time_zone")
|
||||
public String timeZone = "";
|
||||
|
||||
@SerializedName("firmware_version")
|
||||
public String firmwareVersion = "";
|
||||
|
||||
public @NonNullByDefault({}) Health health;
|
||||
|
||||
@SerializedName("battery_life")
|
||||
public String battery = "";
|
||||
|
||||
public class Health {
|
||||
@SerializedName("battery_percentage")
|
||||
public int batteryPercentage;
|
||||
}
|
||||
|
||||
private RingDeviceTO() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.ApiConstants;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Chris Milbert - stickupcam contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class RingDevices {
|
||||
private List<Doorbell> doorbells = new ArrayList<>();
|
||||
private List<Stickupcam> stickupcams = new ArrayList<>();
|
||||
private List<Chime> chimes = new ArrayList<>();
|
||||
private List<OtherDevice> otherdevices = new ArrayList<>();
|
||||
|
||||
public RingDevices(JsonObject jsonRingDevices, RingAccount ringAccount) {
|
||||
addDoorbells((JsonArray) jsonRingDevices.get(ApiConstants.DEVICES_DOORBOTS), ringAccount);
|
||||
addStickupCams((JsonArray) jsonRingDevices.get(ApiConstants.DEVICES_STICKUP_CAMS), ringAccount);
|
||||
addChimes((JsonArray) jsonRingDevices.get(ApiConstants.DEVICES_CHIMES), ringAccount);
|
||||
addOtherDevices((JsonArray) jsonRingDevices.get(ApiConstants.DEVICES_OTHERDEVICE), ringAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create the doorbell list.
|
||||
*
|
||||
* @param jsonDoorbells
|
||||
* @param ringAccount
|
||||
*/
|
||||
private void addDoorbells(JsonArray jsonDoorbells, RingAccount ringAccount) {
|
||||
for (Object obj : jsonDoorbells) {
|
||||
Doorbell doorbell = new Doorbell((JsonObject) obj, ringAccount);
|
||||
doorbells.add(doorbell);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Doorbells Collection.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<Doorbell> getDoorbells() {
|
||||
return doorbells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create the stickupcam list.
|
||||
*
|
||||
* @param jsonStickupcams
|
||||
* @param ringAccount
|
||||
*/
|
||||
private void addStickupCams(JsonArray jsonStickupcams, RingAccount ringAccount) {
|
||||
for (Object obj : jsonStickupcams) {
|
||||
Stickupcam stickupcam = new Stickupcam((JsonObject) obj, ringAccount);
|
||||
stickupcams.add(stickupcam);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Stickupcams Collection.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<Stickupcam> getStickupcams() {
|
||||
return stickupcams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create the chime list.
|
||||
*
|
||||
* @param jsonChimes
|
||||
* @param ringAccount
|
||||
*/
|
||||
private void addChimes(JsonArray jsonChimes, RingAccount ringAccount) {
|
||||
for (Object obj : jsonChimes) {
|
||||
Chime chime = new Chime((JsonObject) obj, ringAccount);
|
||||
chimes.add(chime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Chimes Collection.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<Chime> getChimes() {
|
||||
return chimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create the other list.
|
||||
*
|
||||
* @param jsonOther
|
||||
* @param ringAccount
|
||||
*/
|
||||
private void addOtherDevices(JsonArray jsonOtherDevices, RingAccount ringAccount) {
|
||||
for (Object obj : jsonOtherDevices) {
|
||||
OtherDevice otherdevice = new OtherDevice((JsonObject) obj, ringAccount);
|
||||
otherdevices.add(otherdevice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Others Collection.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<OtherDevice> getOtherDevices() {
|
||||
return otherdevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a collection of all devices.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<RingDevice> getRingDevices() {
|
||||
List<RingDevice> result = new ArrayList<>();
|
||||
result.addAll(doorbells);
|
||||
result.addAll(stickupcams);
|
||||
result.addAll(chimes);
|
||||
result.addAll(otherdevices);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RingEventTO {
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
|
||||
public long id = 0;
|
||||
@SerializedName("created_at")
|
||||
public String createdAt = "";
|
||||
public boolean answered;
|
||||
public String kind = "";
|
||||
public boolean favorite;
|
||||
@SerializedName("snapshot_url")
|
||||
public @Nullable String snapshotUrl;
|
||||
public Map<String, String> recording = Map.of();
|
||||
public List<Object> events = List.of();
|
||||
public DoorbotTO doorbot = new DoorbotTO();
|
||||
|
||||
/**
|
||||
* Get the date/time created as String.
|
||||
*
|
||||
* @return the date/time.
|
||||
*/
|
||||
public DateTimeType getCreatedAt() {
|
||||
return new DateTimeType(
|
||||
ZonedDateTime.parse(createdAt, DATE_TIME_FORMATTER).withZoneSameInstant(ZoneId.systemDefault()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ring.internal.RingAccount;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* @author Chris Milbert - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Stickupcam extends AbstractRingDevice {
|
||||
|
||||
/**
|
||||
* Create Stickup Cam instance from JSON object.
|
||||
*
|
||||
* @param jsonStickupcam the JSON Stickup Cam retrieved from the Ring API.
|
||||
* @param ringAccount the Ring Account in use
|
||||
*/
|
||||
public Stickupcam(JsonObject jsonStickupcam, RingAccount ringAccount) {
|
||||
super(jsonStickupcam, ringAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DiscoveryResult object to identify the device as
|
||||
* discovered thing.
|
||||
*
|
||||
* @return the device as DiscoveryResult instance.
|
||||
*/
|
||||
@Override
|
||||
public DiscoveryResult getDiscoveryResult(RingDeviceTO deviceTO) {
|
||||
DiscoveryResult result = DiscoveryResultBuilder
|
||||
.create(new ThingUID("ring:stickupcam:" + getRingAccount().getThingId() + ":" + deviceTO.id))
|
||||
.withLabel("Ring Video Stickup Cam - " + deviceTO.description).build();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.ring.RingBindingConstants.*;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ring.internal.RingDeviceRegistry;
|
||||
import org.openhab.binding.ring.internal.data.RingDevice;
|
||||
import org.openhab.binding.ring.internal.data.RingDeviceTO;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The RingDiscoveryService is responsible for auto detecting a Ring
|
||||
* device in the local network.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Chris Milbert - Stickupcam contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.ring")
|
||||
@NonNullByDefault
|
||||
public class RingDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(RingDiscoveryService.class);
|
||||
private @Nullable ScheduledFuture<?> discoveryJob;
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public RingDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, 5, true);
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
logger.debug("Starting Ring discovery...");
|
||||
startScan();
|
||||
startBackgroundDiscovery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
logger.debug("Stopping Ring discovery...");
|
||||
stopBackgroundDiscovery();
|
||||
stopScan();
|
||||
}
|
||||
|
||||
private void discover() {
|
||||
RingDeviceRegistry registry = RingDeviceRegistry.getInstance();
|
||||
for (RingDevice device : registry.getRingDevices(RingDeviceRegistry.Status.ADDED)) {
|
||||
RingDeviceTO deviceTO = gson.fromJson(device.getJsonObject(), RingDeviceTO.class);
|
||||
if (deviceTO != null) {
|
||||
thingDiscovered(device.getDiscoveryResult(deviceTO));
|
||||
registry.setStatus(deviceTO.id, RingDeviceRegistry.Status.DISCOVERED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
discover();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
discoveryJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, 120, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.info("Stop Ring background discovery");
|
||||
ScheduledFuture<?> job = discoveryJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
discoveryJob = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting device search...");
|
||||
discover();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
super.stopScan();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.errors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* AuthenticationException will be thrown if an invalid username or
|
||||
* password is used to get access to the Ring account.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class AuthenticationException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -2630294607218363771L;
|
||||
|
||||
public AuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.errors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* DeviceNotFoundException will be thrown if an device is requested from
|
||||
* the device registry with an id that is not registered.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DeviceNotFoundException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -463646377949508962L;
|
||||
|
||||
public DeviceNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.errors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* DuplicateIdException will be thrown if an device is added to
|
||||
* the device registry with an id that is already registered.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DuplicateIdException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -4010587859949508962L;
|
||||
|
||||
public DuplicateIdException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.errors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* IllegalDeviceClassException will be thrown if an device is retrieved
|
||||
* from the RingDeviceRegistry and the class is not as expected.
|
||||
* E.g. if a Doorbell is expected, but a Chime is returned.
|
||||
*
|
||||
* @author Wim Vissers - Initial contribution
|
||||
* @author Ben Rosenblum - Updated for OH4 / New Maintainer
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class IllegalDeviceClassException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -4010587859949508962L;
|
||||
|
||||
public IllegalDeviceClassException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.utils;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link RingDoorbellHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Ben Rosenblum - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RingUtils {
|
||||
|
||||
public static String sanitizeData(@Nullable String sensitive) {
|
||||
if (sensitive == null) {
|
||||
return "NULL";
|
||||
} else if ("".equals(sensitive)) {
|
||||
return "STRINGEMPTY";
|
||||
} else {
|
||||
return "NOTEMPTY";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="ring" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Ring Binding</name>
|
||||
<description>This is the addon for ring.com devices like the Video Doorbell</description>
|
||||
<connection>cloud</connection>
|
||||
|
||||
</addon:addon>
|
|
@ -0,0 +1,87 @@
|
|||
# add-on
|
||||
|
||||
addon.ring.name = Ring Binding
|
||||
addon.ring.description = This is the addon for ring.com devices like the Video Doorbell
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.ring.account.label = Binding Bridge
|
||||
thing-type.ring.account.description = Account with ring.com to access the devices
|
||||
thing-type.ring.chime.label = Chime Binding Thing
|
||||
thing-type.ring.chime.description = Ring Chime connected to the system
|
||||
thing-type.ring.doorbell.label = Video Doorbell Binding Thing
|
||||
thing-type.ring.doorbell.description = A Ring Video Doorbell device
|
||||
thing-type.ring.other.label = Other Binding Thing
|
||||
thing-type.ring.other.description = A Ring Other device
|
||||
thing-type.ring.stickupcam.label = Stickup Cam Binding Thing
|
||||
thing-type.ring.stickupcam.description = A Ring Stickup Cam device
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.ring.account.hardwareId.label = A unique hardware id
|
||||
thing-type.config.ring.account.hardwareId.description = Enter a hardware id that is unique for all your devices connected to Ring (e.g. computer's MAC address)
|
||||
thing-type.config.ring.account.password.label = Account's Password
|
||||
thing-type.config.ring.account.password.description = Enter the password you used to subscribe to the Ring services. If using 2 factor authentication, leave this blank and enter the refresh token instead.
|
||||
thing-type.config.ring.account.refreshInterval.label = Refresh interval
|
||||
thing-type.config.ring.account.refreshInterval.description = How often to poll the Ring service for events in seconds
|
||||
thing-type.config.ring.account.twofactorCode.label = 2 factor authentication code
|
||||
thing-type.config.ring.account.twofactorCode.description = Enter the 2 factor authentication code, if enabled
|
||||
thing-type.config.ring.account.username.label = User Name
|
||||
thing-type.config.ring.account.username.description = Enter the user name you used to subscribe to the Ring services. If using 2 factor authentication, leave this blank and enter the refresh token instead.
|
||||
thing-type.config.ring.account.videoRetentionCount.label = Number of videos to keep
|
||||
thing-type.config.ring.account.videoRetentionCount.description = The number of video files to keep when automatically downloading the latest event video, or 0 to disable auto downloading
|
||||
thing-type.config.ring.account.videoStoragePath.label = Video Download Path
|
||||
thing-type.config.ring.account.videoStoragePath.description = The folder path to save video .mp4 files in when downloaded from ring.com. Note: the openhab user must have rights to save to this location
|
||||
thing-type.config.ring.chime.offOffset.label = Power-off Time
|
||||
thing-type.config.ring.chime.offOffset.description = Offset in minutes to switch off
|
||||
thing-type.config.ring.chime.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.ring.chime.refreshInterval.description = How often to poll the Ring service for events in seconds
|
||||
thing-type.config.ring.doorbell.offOffset.label = Power-off Time
|
||||
thing-type.config.ring.doorbell.offOffset.description = Offset in minutes to switch off
|
||||
thing-type.config.ring.doorbell.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.ring.doorbell.refreshInterval.description = How often to poll the Ring service for events in seconds
|
||||
thing-type.config.ring.other.offOffset.label = Power-off Time
|
||||
thing-type.config.ring.other.offOffset.description = Offset in minutes to switch off
|
||||
thing-type.config.ring.other.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.ring.other.refreshInterval.description = How often to poll the Ring service for events in seconds
|
||||
thing-type.config.ring.stickupcam.offOffset.label = Power-off Time
|
||||
thing-type.config.ring.stickupcam.offOffset.description = Offset in minutes to switch off
|
||||
thing-type.config.ring.stickupcam.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.ring.stickupcam.refreshInterval.description = How often to poll the Ring service for events in seconds
|
||||
|
||||
# channel group types
|
||||
|
||||
channel-group-type.ring.controlGroup.label = Control
|
||||
channel-group-type.ring.controlGroup.description = Operational control and status information
|
||||
channel-group-type.ring.deviceStatus.label = Device Status
|
||||
channel-group-type.ring.deviceStatus.description = Device Status Information
|
||||
channel-group-type.ring.eventGroup.label = Events
|
||||
channel-group-type.ring.eventGroup.description = Currently selected event information
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.ring.battery.label = Battery Level
|
||||
channel-type.ring.battery.description = Battery level in %
|
||||
channel-type.ring.createdAt.label = Event DateTime
|
||||
channel-type.ring.createdAt.description = The date and time the event was created
|
||||
channel-type.ring.createdAt.state.pattern = %1$tF %1$tR
|
||||
channel-type.ring.doorbotDescription.label = Event Device Name
|
||||
channel-type.ring.doorbotDescription.description = The description of the Ring device (doorbell, chime, etc) that generated the currently selected event
|
||||
channel-type.ring.doorbotId.label = Event Device ID
|
||||
channel-type.ring.doorbotId.description = The id of the Ring device (doorbell, chime, etc) that generated the currently selected event
|
||||
channel-type.ring.enabled.label = Enable Polling
|
||||
channel-type.ring.enabled.description = Account Polling Enabled (on=yes, off=no)
|
||||
channel-type.ring.kind.label = Event Type
|
||||
channel-type.ring.kind.description = The kind of event, usually 'motion' or 'ding'
|
||||
channel-type.ring.url.label = URL to recorded video
|
||||
channel-type.ring.url.description = The URL to a recorded video (only when subscribed)
|
||||
|
||||
# binding
|
||||
|
||||
binding.ring.name = Ring Binding
|
||||
binding.ring.description = This is the binding for ring.com devices like the Video Doorbell
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.ring.account.refreshToken.label = The Ring account's refresh token
|
||||
thing-type.config.ring.account.refreshToken.description = Enter the refresh token from Ring. Use this instead of username/password when using 2 factor authentication, or if you don't wish to save the username and password in OpenHAB
|
|
@ -0,0 +1,272 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="ring"
|
||||
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">
|
||||
|
||||
<!-- Account Bridge Type -->
|
||||
<bridge-type id="account">
|
||||
<label>Ring Account Bridge</label>
|
||||
<description>Account with ring.com to access the devices</description>
|
||||
<semantic-equipment-tag>WebService</semantic-equipment-tag>
|
||||
<channel-groups>
|
||||
<channel-group id="control" typeId="controlGroup"/>
|
||||
<channel-group id="event" typeId="eventGroup"/>
|
||||
</channel-groups>
|
||||
<config-description>
|
||||
<parameter name="username" type="text" required="false">
|
||||
<label>User Name</label>
|
||||
<description>Enter the user name you used to subscribe to the Ring services. If using 2 factor authentication, leave
|
||||
this blank and enter the refresh token instead.</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="false">
|
||||
<label>Account's Password</label>
|
||||
<description>Enter the password you used to subscribe to the Ring services. If using 2 factor authentication, leave
|
||||
this blank and enter the refresh token instead.</description>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
<parameter name="twofactorCode" type="text" required="false">
|
||||
<context>password</context>
|
||||
<label>2 factor authentication code</label>
|
||||
<description>Enter the 2 factor authentication code, if enabled</description>
|
||||
</parameter>
|
||||
<parameter name="hardwareId" type="text">
|
||||
<label>A unique hardware id</label>
|
||||
<description>Enter a hardware id that is unique for all your devices connected to Ring (e.g. computer's MAC address)</description>
|
||||
<default></default>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh interval</label>
|
||||
<description>How often to poll the Ring service for events in seconds</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
<parameter name="videoStoragePath" type="text">
|
||||
<label>Video Download Path</label>
|
||||
<description>The folder path to save video .mp4 files in when downloaded from ring.com. Note: the openhab user must
|
||||
have rights to save to this location</description>
|
||||
</parameter>
|
||||
<parameter name="videoRetentionCount" type="integer">
|
||||
<label>Number of videos to keep</label>
|
||||
<description>The number of video files to keep when automatically downloading the latest event video, or 0 to
|
||||
disable auto downloading</description>
|
||||
<default>10</default>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
<!-- Other Device Thing Type -->
|
||||
<thing-type id="otherdevice">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Other Device Binding Thing</label>
|
||||
<description>A Ring Other Device device</description>
|
||||
<semantic-equipment-tag>Camera</semantic-equipment-tag>
|
||||
<channel-groups>
|
||||
<channel-group id="control" typeId="controlGroup"/>
|
||||
<channel-group id="status" typeId="deviceStatus"/>
|
||||
</channel-groups>
|
||||
<properties>
|
||||
<property name="Description">unknown</property>
|
||||
<property name="Device ID">unknown</property>
|
||||
<property name="Kind">unknown</property>
|
||||
</properties>
|
||||
<config-description>
|
||||
<parameter name="offOffset" type="decimal">
|
||||
<label>Power-off Time</label>
|
||||
<description>Offset in minutes to switch off</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh Interval</label>
|
||||
<description>How often to poll the Ring service for events in seconds</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
<!-- Ring Chime Thing Type -->
|
||||
<thing-type id="chime">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Chime Binding Thing</label>
|
||||
<description>Ring Chime connected to the system</description>
|
||||
<semantic-equipment-tag>Camera</semantic-equipment-tag>
|
||||
<channel-groups>
|
||||
<channel-group id="control" typeId="controlGroup"/>
|
||||
</channel-groups>
|
||||
<properties>
|
||||
<property name="Description">unknown</property>
|
||||
<property name="Device ID">unknown</property>
|
||||
<property name="Kind">unknown</property>
|
||||
</properties>
|
||||
<config-description>
|
||||
<parameter name="offOffset" type="decimal">
|
||||
<label>Power-off Time</label>
|
||||
<description>Offset in minutes to switch off</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh Interval</label>
|
||||
<description>How often to poll the Ring service for events in seconds</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
<!-- Doorbell Thing Type -->
|
||||
<thing-type id="doorbell">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Video Doorbell Binding Thing</label>
|
||||
<description>A Ring Video Doorbell device</description>
|
||||
<semantic-equipment-tag>Camera</semantic-equipment-tag>
|
||||
<channel-groups>
|
||||
<channel-group id="control" typeId="controlGroup"/>
|
||||
<channel-group id="status" typeId="deviceStatus"/>
|
||||
</channel-groups>
|
||||
<properties>
|
||||
<property name="Description">unknown</property>
|
||||
<property name="Device ID">unknown</property>
|
||||
<property name="Kind">unknown</property>
|
||||
</properties>
|
||||
<config-description>
|
||||
<parameter name="offOffset" type="decimal">
|
||||
<label>Power-off Time</label>
|
||||
<description>Offset in minutes to switch off</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh Interval</label>
|
||||
<description>How often to poll the Ring service for events in seconds</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
<!-- StickupCam Thing Type -->
|
||||
<thing-type id="stickupcam">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Stickup Cam Binding Thing</label>
|
||||
<description>A Ring Stickup Cam device</description>
|
||||
<semantic-equipment-tag>Camera</semantic-equipment-tag>
|
||||
<channel-groups>
|
||||
<channel-group id="control" typeId="controlGroup"/>
|
||||
<channel-group id="status" typeId="deviceStatus"/>
|
||||
</channel-groups>
|
||||
<properties>
|
||||
<property name="Description">unknown</property>
|
||||
<property name="Device ID">unknown</property>
|
||||
<property name="Kind">unknown</property>
|
||||
</properties>
|
||||
<config-description>
|
||||
<parameter name="offOffset" type="decimal">
|
||||
<label>Power-off Time</label>
|
||||
<description>Offset in minutes to switch off</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh Interval</label>
|
||||
<description>How often to poll the Ring service for events in seconds</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Channel Groups -->
|
||||
<channel-group-type id="controlGroup">
|
||||
<label>Control</label>
|
||||
<description>Operational control and status information</description>
|
||||
<channels>
|
||||
<channel id="enabled" typeId="enabled"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="deviceStatus">
|
||||
<label>Device Status</label>
|
||||
<description>Device Status Information</description>
|
||||
<channels>
|
||||
<channel id="battery" typeId="battery"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="eventGroup">
|
||||
<label>Events</label>
|
||||
<description>Currently selected event information</description>
|
||||
<channels>
|
||||
<channel id="url" typeId="url"/>
|
||||
<channel id="createdAt" typeId="createdAt"/>
|
||||
<channel id="kind" typeId="kind"/>
|
||||
<channel typeId="doorbotId" id="doorbotId"></channel>
|
||||
<channel typeId="doorbotDescription" id="doorbotDescription"></channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<!-- Channel Types -->
|
||||
<channel-type id="url">
|
||||
<item-type>String</item-type>
|
||||
<label>URL to recorded video</label>
|
||||
<description>The URL to a recorded video (only when subscribed)</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Info</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
<channel-type id="doorbotId">
|
||||
<item-type>String</item-type>
|
||||
<label>Event Device ID</label>
|
||||
<description>The id of the Ring device (doorbell, chime, etc) that generated the currently selected event</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Info</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
<channel-type id="doorbotDescription">
|
||||
<item-type>String</item-type>
|
||||
<label>Event Device Name</label>
|
||||
<description>The description of the Ring device (doorbell, chime, etc) that generated the currently selected event</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Info</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
<channel-type id="createdAt">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Event DateTime</label>
|
||||
<description>The date and time the event was created</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Timestamp</tag>
|
||||
</tags>
|
||||
<state pattern="%1$tF %1$tR" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="kind">
|
||||
<item-type>String</item-type>
|
||||
<label>Event Type</label>
|
||||
<description>The kind of event, usually 'motion' or 'ding'</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Info</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
<channel-type id="enabled">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Enable Polling</label>
|
||||
<description>Account Polling Enabled (on=yes, off=no)</description>
|
||||
<tags>
|
||||
<tag>Switch</tag>
|
||||
<tag>Info</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
<channel-type id="battery">
|
||||
<item-type>Number</item-type>
|
||||
<label>Battery Level</label>
|
||||
<description>Battery level in %</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Level</tag>
|
||||
</tags>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.ring.internal.data;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.ring.internal.RestClient;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link DeserializeTest} class contains de-serialization tests
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class DeserializeTest {
|
||||
private static final String RESOURCE_PATH = "src" + File.separator + "test" + File.separator + "resources"
|
||||
+ File.separator;
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@Test
|
||||
void testEventDeserialization() throws FileNotFoundException {
|
||||
String input = new Scanner(new File(RESOURCE_PATH + "event_response.json")).useDelimiter("\\Z").next();
|
||||
|
||||
List<RingEventTO> events = Objects.requireNonNull(gson.fromJson(input, RestClient.RING_EVENT_LIST_TYPE));
|
||||
|
||||
assertThat(events.size(), is(1));
|
||||
RingEventTO event = events.getFirst();
|
||||
|
||||
assertThat(event.id, is(7511772057612656721L));
|
||||
assertThat(event.getCreatedAt().getZonedDateTime(ZoneId.of("GMT")),
|
||||
equalTo(ZonedDateTime.of(2025, 6, 3, 17, 12, 3, 567000000, ZoneId.of("GMT"))));
|
||||
assertThat(event.kind, equalTo("ding"));
|
||||
assertThat(event.doorbot.id, equalTo("6000000000"));
|
||||
assertThat(event.doorbot.description, equalTo("Haustür"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
[
|
||||
{
|
||||
"id": 7511772057612656721,
|
||||
"created_at": "2025-06-03T17:12:03.567Z",
|
||||
"answered": true,
|
||||
"events": [],
|
||||
"kind": "ding",
|
||||
"favorite": false,
|
||||
"snapshot_url": "",
|
||||
"recording": {
|
||||
"status": "ready"
|
||||
},
|
||||
"duration": 62.0,
|
||||
"cv_properties": {
|
||||
"person_detected": null,
|
||||
"stream_broken": false,
|
||||
"detection_type": null,
|
||||
"detection_types": null,
|
||||
"security_alerts": null,
|
||||
"full_description": null,
|
||||
"short_description": null
|
||||
},
|
||||
"properties": {
|
||||
"is_alexa": false,
|
||||
"is_sidewalk": false,
|
||||
"is_autoreply": false,
|
||||
"stark_reviewed": false
|
||||
},
|
||||
"doorbot": {
|
||||
"id": 6000000000,
|
||||
"description": "Haustür",
|
||||
"type": "df_doorbell_clownfish"
|
||||
},
|
||||
"device_placement": null,
|
||||
"geolocation": null,
|
||||
"last_location": null,
|
||||
"siren": null,
|
||||
"is_e2ee": false,
|
||||
"had_subscription": true,
|
||||
"owner_id": "100000000",
|
||||
"riid": "00000000000000000000000000000000"
|
||||
}
|
||||
]
|
|
@ -362,6 +362,7 @@
|
|||
<module>org.openhab.binding.renault</module>
|
||||
<module>org.openhab.binding.resol</module>
|
||||
<module>org.openhab.binding.rfxcom</module>
|
||||
<module>org.openhab.binding.ring</module>
|
||||
<module>org.openhab.binding.rme</module>
|
||||
<module>org.openhab.binding.robonect</module>
|
||||
<module>org.openhab.binding.roku</module>
|
||||
|
|
Loading…
Reference in New Issue