2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.surepetcare.internal.handler;
15 import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*;
17 import java.io.IOException;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
21 import javax.measure.quantity.Mass;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.surepetcare.internal.SurePetcareAPIHelper;
26 import org.openhab.binding.surepetcare.internal.SurePetcareApiException;
27 import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice;
28 import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold;
29 import org.openhab.binding.surepetcare.internal.dto.SurePetcarePet;
30 import org.openhab.binding.surepetcare.internal.dto.SurePetcarePetActivity;
31 import org.openhab.binding.surepetcare.internal.dto.SurePetcarePetFeeding;
32 import org.openhab.binding.surepetcare.internal.dto.SurePetcareTag;
33 import org.openhab.core.cache.ByteArrayFileCache;
34 import org.openhab.core.io.net.http.HttpUtil;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.types.RawType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.unit.SIUnits;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link SurePetcarePetHandler} is responsible for handling the things created to represent Sure Petcare pets.
53 * @author Rene Scherer - Initial Contribution
54 * @author Holger Eisold - Added pet feeder status, location time offset
57 public class SurePetcarePetHandler extends SurePetcareBaseObjectHandler {
59 private final Logger logger = LoggerFactory.getLogger(SurePetcarePetHandler.class);
61 private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.surepetcare");
63 public SurePetcarePetHandler(Thing thing, SurePetcareAPIHelper petcareAPI) {
64 super(thing, petcareAPI);
68 public void handleCommand(ChannelUID channelUID, Command command) {
69 if (command instanceof RefreshType) {
70 updateThingCache.getValue();
72 switch (channelUID.getId()) {
73 case PET_CHANNEL_LOCATION:
74 logger.debug("Received location update command: {}", command.toString());
75 if (command instanceof StringType) {
76 synchronized (petcareAPI) {
77 SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId());
79 String newLocationIdStr = ((StringType) command).toString();
81 Integer newLocationId = Integer.valueOf(newLocationIdStr);
82 // Only update if location has changed. (Needed for Group:Switch item)
83 if ((pet.status.activity.where.equals(newLocationId)) || newLocationId.equals(0)) {
84 logger.debug("Location has not changed, skip pet id: {} with loc id: {}",
85 pet.id, newLocationId);
87 logger.debug("Received new location: {}", newLocationId);
88 petcareAPI.setPetLocation(pet, newLocationId, ZonedDateTime.now());
89 updateState(PET_CHANNEL_LOCATION,
90 new StringType(pet.status.activity.where.toString()));
91 updateState(PET_CHANNEL_LOCATION_CHANGED,
92 new DateTimeType(pet.status.activity.since));
94 } catch (NumberFormatException e) {
95 logger.warn("Invalid location id: {}, ignoring command", newLocationIdStr, e);
96 } catch (SurePetcareApiException e) {
97 logger.warn("Error from SurePetcare API. Can't update location {} for pet {}",
98 newLocationIdStr, pet, e);
104 case PET_CHANNEL_LOCATION_TIMEOFFSET:
105 logger.debug("Received location time offset update command: {}", command.toString());
106 if (command instanceof StringType) {
107 synchronized (petcareAPI) {
108 SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId());
110 String commandIdStr = ((StringType) command).toString();
112 Integer commandId = Integer.valueOf(commandIdStr);
113 Integer currentLocation = pet.status.activity.where;
114 logger.debug("Received new location: {}", currentLocation == 1 ? 2 : 1);
115 // We set the location to the opposite state.
116 // We also set location to INSIDE (1) if currentLocation is Unknown (0)
117 if (commandId == 10 || commandId == 30 || commandId == 60) {
118 ZonedDateTime time = ZonedDateTime.now().minusMinutes(commandId);
119 petcareAPI.setPetLocation(pet, currentLocation == 1 ? 2 : 1, time);
122 updateState(PET_CHANNEL_LOCATION,
123 new StringType(pet.status.activity.where.toString()));
124 updateState(PET_CHANNEL_LOCATION_CHANGED,
125 new DateTimeType(pet.status.activity.since));
126 updateState(PET_CHANNEL_LOCATION_TIMEOFFSET, UnDefType.UNDEF);
127 } catch (NumberFormatException e) {
128 logger.warn("Invalid location id: {}, ignoring command", commandIdStr, e);
129 } catch (SurePetcareApiException e) {
130 logger.warn("Error from SurePetcare API. Can't update location {} for pet {}",
131 commandIdStr, pet, e);
138 logger.warn("Update on unsupported channel {}", channelUID.getId());
144 protected void updateThing() {
145 synchronized (petcareAPI) {
146 SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId());
148 logger.debug("Updating all thing channels for pet : {}", pet);
149 updateState(PET_CHANNEL_ID, new DecimalType(pet.id));
150 updateState(PET_CHANNEL_NAME, pet.name == null ? UnDefType.UNDEF : new StringType(pet.name));
151 updateState(PET_CHANNEL_COMMENT, pet.comments == null ? UnDefType.UNDEF : new StringType(pet.comments));
152 updateState(PET_CHANNEL_GENDER,
153 pet.genderId == null ? UnDefType.UNDEF : new StringType(pet.genderId.toString()));
154 updateState(PET_CHANNEL_BREED,
155 pet.breedId == null ? UnDefType.UNDEF : new StringType(pet.breedId.toString()));
156 updateState(PET_CHANNEL_SPECIES,
157 pet.speciesId == null ? UnDefType.UNDEF : new StringType(pet.speciesId.toString()));
158 updateState(PET_CHANNEL_PHOTO,
159 pet.photo == null ? UnDefType.UNDEF : getPetPhotoFromCache(pet.photo.location));
161 SurePetcarePetActivity loc = pet.status.activity;
163 updateState(PET_CHANNEL_LOCATION, new StringType(loc.where.toString()));
164 if (loc.since != null) {
165 updateState(PET_CHANNEL_LOCATION_CHANGED, new DateTimeType(loc.since));
168 updateState(PET_CHANNEL_DATE_OF_BIRTH, pet.dateOfBirth == null ? UnDefType.UNDEF
169 : new DateTimeType(pet.dateOfBirth.atStartOfDay(ZoneId.systemDefault())));
170 updateState(PET_CHANNEL_WEIGHT,
171 pet.weight == null ? UnDefType.UNDEF : new QuantityType<Mass>(pet.weight, SIUnits.KILOGRAM));
172 if (pet.tagId != null) {
173 SurePetcareTag tag = petcareAPI.getTag(pet.tagId.toString());
175 updateState(PET_CHANNEL_TAG_IDENTIFIER, new StringType(tag.tag));
178 if (pet.status.activity.deviceId != null) {
179 SurePetcareDevice device = petcareAPI.getDevice(pet.status.activity.deviceId.toString());
180 if (device != null) {
181 updateState(PET_CHANNEL_LOCATION_CHANGED_THROUGH, new StringType(device.name));
183 } else if (pet.status.activity.userId != null) {
184 SurePetcareHousehold household = petcareAPI.getHousehold(pet.householdId.toString());
185 if (household != null) {
186 Long userId = pet.status.activity.userId;
187 household.users.stream().map(user -> user.user).filter(user -> userId.equals(user.userId))
188 .forEach(user -> updateState(PET_CHANNEL_LOCATION_CHANGED_THROUGH,
189 new StringType(user.userName)));
192 SurePetcarePetFeeding feeding = pet.status.feeding;
193 if (feeding != null) {
194 SurePetcareDevice device = petcareAPI.getDevice(feeding.deviceId.toString());
195 if (device != null) {
196 updateState(PET_CHANNEL_FEEDER_DEVICE, new StringType(device.name));
197 int bowlId = device.control.bowls.bowlId;
198 int numBowls = feeding.feedChange.size();
200 if (bowlId == BOWL_ID_ONE_BOWL_USED) {
201 updateState(PET_CHANNEL_FEEDER_LAST_CHANGE,
202 new QuantityType<Mass>(feeding.feedChange.get(0), SIUnits.GRAM));
203 } else if (bowlId == BOWL_ID_TWO_BOWLS_USED) {
204 updateState(PET_CHANNEL_FEEDER_LAST_CHANGE_LEFT,
205 new QuantityType<Mass>(feeding.feedChange.get(0), SIUnits.GRAM));
207 updateState(PET_CHANNEL_FEEDER_LAST_CHANGE_RIGHT,
208 new QuantityType<Mass>(feeding.feedChange.get(1), SIUnits.GRAM));
212 updateState(PET_CHANNEL_FEEDER_LASTFEEDING, new DateTimeType(feeding.feedChangeAt));
216 logger.debug("Trying to update unknown pet: {}", thing.getUID().getId());
222 * Tries to lookup image in cache. If not found, it tries to download the image from its URL.
224 * @param url the url of the pet photo
225 * @return the pet image as {@link RawType} or UNDEF
227 private State getPetPhotoFromCache(@Nullable String url) {
229 return UnDefType.UNDEF;
231 if (IMAGE_CACHE.containsKey(url)) {
233 byte[] bytes = IMAGE_CACHE.get(url);
234 String contentType = HttpUtil.guessContentTypeFromData(bytes);
235 return new RawType(bytes,
236 contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType);
237 } catch (IOException e) {
238 logger.trace("Failed to download the content of URL '{}'", url, e);
241 // photo is not yet in cache, download and add
242 RawType image = HttpUtil.downloadImage(url);
244 IMAGE_CACHE.put(url, image.getBytes());
248 return UnDefType.UNDEF;