2 * Copyright (c) 2010-2023 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 commandAsStringType) {
76 synchronized (petcareAPI) {
77 SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId());
79 String newLocationIdStr = commandAsStringType.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 commandAsStringType) {
107 synchronized (petcareAPI) {
108 SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId());
110 String commandIdStr = commandAsStringType.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 if (loc.deviceId != null) {
169 SurePetcareDevice device = petcareAPI.getDevice(loc.deviceId.toString());
170 if (device != null) {
171 updateState(PET_CHANNEL_LOCATION_CHANGED_THROUGH, new StringType(device.name));
173 } else if (loc.userId != null) {
174 SurePetcareHousehold household = petcareAPI.getHousehold(pet.householdId.toString());
175 if (household != null) {
176 Long userId = loc.userId;
177 household.users.stream().map(user -> user.user).filter(user -> userId.equals(user.userId))
178 .forEach(user -> updateState(PET_CHANNEL_LOCATION_CHANGED_THROUGH,
179 new StringType(user.userName)));
183 updateState(PET_CHANNEL_DATE_OF_BIRTH, pet.dateOfBirth == null ? UnDefType.UNDEF
184 : new DateTimeType(pet.dateOfBirth.atStartOfDay(ZoneId.systemDefault())));
185 updateState(PET_CHANNEL_WEIGHT,
186 pet.weight == null ? UnDefType.UNDEF : new QuantityType<Mass>(pet.weight, SIUnits.KILOGRAM));
187 if (pet.tagId != null) {
188 SurePetcareTag tag = petcareAPI.getTag(pet.tagId.toString());
190 updateState(PET_CHANNEL_TAG_IDENTIFIER, new StringType(tag.tag));
193 SurePetcarePetFeeding feeding = pet.status.feeding;
194 if (feeding != null) {
195 SurePetcareDevice device = petcareAPI.getDevice(feeding.deviceId.toString());
196 if (device != null) {
197 updateState(PET_CHANNEL_FEEDER_DEVICE, new StringType(device.name));
198 int bowlId = device.control.bowls.bowlId;
199 int numBowls = feeding.feedChange.size();
201 if (bowlId == BOWL_ID_ONE_BOWL_USED) {
202 updateState(PET_CHANNEL_FEEDER_LAST_CHANGE,
203 new QuantityType<Mass>(feeding.feedChange.get(0), SIUnits.GRAM));
204 } else if (bowlId == BOWL_ID_TWO_BOWLS_USED) {
205 updateState(PET_CHANNEL_FEEDER_LAST_CHANGE_LEFT,
206 new QuantityType<Mass>(feeding.feedChange.get(0), SIUnits.GRAM));
208 updateState(PET_CHANNEL_FEEDER_LAST_CHANGE_RIGHT,
209 new QuantityType<Mass>(feeding.feedChange.get(1), SIUnits.GRAM));
213 updateState(PET_CHANNEL_FEEDER_LASTFEEDING, new DateTimeType(feeding.feedChangeAt));
217 logger.debug("Trying to update unknown pet: {}", thing.getUID().getId());
223 * Tries to lookup image in cache. If not found, it tries to download the image from its URL.
225 * @param url the url of the pet photo
226 * @return the pet image as {@link RawType} or UNDEF
228 private State getPetPhotoFromCache(@Nullable String url) {
230 return UnDefType.UNDEF;
232 if (IMAGE_CACHE.containsKey(url)) {
234 byte[] bytes = IMAGE_CACHE.get(url);
235 String contentType = HttpUtil.guessContentTypeFromData(bytes);
236 return new RawType(bytes,
237 contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType);
238 } catch (IOException e) {
239 logger.trace("Failed to download the content of URL '{}'", url, e);
242 // photo is not yet in cache, download and add
243 RawType image = HttpUtil.downloadImage(url);
245 IMAGE_CACHE.put(url, image.getBytes());
249 return UnDefType.UNDEF;