diff --git a/CODEOWNERS b/CODEOWNERS index 82d963dc34f..2a8d2cbeaf9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -258,6 +258,7 @@ /bundles/org.openhab.binding.sonyprojector/ @lolodomo /bundles/org.openhab.binding.spotify/ @Hilbrand /bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush +/bundles/org.openhab.binding.surepetcare/ @renescherer @HerzScheisse /bundles/org.openhab.binding.synopanalyzer/ @clinique /bundles/org.openhab.binding.systeminfo/ @svilenvul /bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index c7dbcefbccc..1b0f7a7afa2 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1271,6 +1271,11 @@ org.openhab.binding.squeezebox ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.surepetcare + ${project.version} + org.openhab.addons.bundles org.openhab.binding.synopanalyzer diff --git a/bundles/org.openhab.binding.surepetcare/NOTICE b/bundles/org.openhab.binding.surepetcare/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.surepetcare/README.md b/bundles/org.openhab.binding.surepetcare/README.md new file mode 100644 index 00000000000..4a6e85a103f --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/README.md @@ -0,0 +1,383 @@ +# Sure Petcare Binding + +This binding offers integration to the Sure Petcare API, supporting cloud-connected cat flaps and feeders. + +### Features + +1. Read access to all attributes for households, devices (hubs, flaps) and pets through individual things/channels. +2. Manual setting of pet location. +3. Setting of LED Mode (hub), Locking Mode (flaps) and Curfews. + +### Restrictions / TODO + +1. The Sure Petcare API is not publicly available and this binding has been based on observed interactions between their mobile phone app and the cloud API. + If the Sure Petcare API changes, this binding might stop working. +2. The current version of the binding supports only cat/pet flaps. Feeders are not yet supported as I don't own one yet. + +### Credits + +The binding code is based on a lot of work done by other developers: + +- Holger Eisold (https://github.com/HerzScheisse) - Python use in openHAB and various PRs (https://github.com/HerzScheisse/SurePetcare-openHAB-JSR223-Rules) +- Alex Toft (https://github.com/alextoft) - PHP implementation (https://github.com/alextoft/sureflap) +- rcastberg (https://github.com/rcastberg) - Python implementation (https://github.com/rcastberg/sure_petcare) + +## Supported Things + +This binding supports the following thing types + +| Thing | Thing Type | Discovery | Description | +|-----------------|------------|-----------|--------------------------------------------------------------------------| +| Bridge | Bridge | Manual | A single connection to the Sure Petcare API | +| Household | Thing | Automatic | The Sure Petcare Household | +| Hub Device | Thing | Automatic | The hub device which connects the cat flaps and feeders to the internet | +| Flap Device | Thing | Automatic | A cat or pet flap | +| Feeder Device | Thing | Automatic | A pet feeder | +| Pet | Thing | Automatic | A pet (dog or cat) | + +## Getting started / Discovery + +The binding consists of a Bridge (the API connection), and a number of Things, which relates to the individual hardware devices and pets. +Sure Petcare things can be configured either through the online configuration utility via discovery, or manually through a 'surepetcare.things' configuration file. +The Bridge will not be autodiscovered and must be added manually. This can be done via bridge thing configuration file or via PaperUI. That is because the Sure Petcare API requires authentication credentials to communicate with the service. + +After adding the Bridge, it will go ONLINE, and after a short while, the discovery process for household, devices and pets will start. When new hardware is discovered it will appear in the Inbox. + +## Things and their channels + +Channel names in **bold** are read/write, everything else is read-only + +### Bridge Thing + +| Channel | Type | Description | +|-------------|--------|-----------------------------------------------------------------------------------| +| refresh | Switch | Trigger switch to force a full cache update | + +### Household Thing + +| Channel | Type | Description | +|------------|----------|----------------------------------------------| +| id | Number | A unique id assigned by the Sure Petcare API | +| name | Text | The name of the household | +| timezoneId | Number | The id of the household's timezone | + +### Hub Device Thing + +| Channel | Type | Description | +|-----------------|----------|-----------------------------------------------------------------------| +| id | Number | A unique id assigned by the Sure Petcare API | +| name | Text | The name of the hub | +| product | Text | The type of product (1=hub) | +| ledMode | Text | The mode of the hub's LED ears | +| pairingMode | Text | The state of pairing | +| online | Switch | Indicator if the hub is connected to the internet | + +### Flap Device Thing (Cat or Pet Flap) + +| Channel | Type | Description | +|-----------------------|----------|-----------------------------------------------------------------------| +| id | Number | A unique id assigned by the Sure Petcare API | +| name | Text | The name of the flap | +| product | Text | The type of product (3=pet flap, 6=cat flap) | +| curfewEnabled1 | Switch | Indicator if curfew #1 configuration is enabled | +| curfewLockTime1 | Text | The curfew #1 locking time (HH:MM) | +| curfewUnlockTime1 | Text | The curfew #1 unlocking time (HH:MM) | +| curfewEnabled2 | Switch | Indicator if curfew #2 configuration is enabled | +| curfewLockTime2 | Text | The curfew #2 locking time (HH:MM) | +| curfewUnlockTime2 | Text | The curfew #2 unlocking time (HH:MM) | +| curfewEnabled3 | Switch | Indicator if curfew #3 configuration is enabled | +| curfewLockTime3 | Text | The curfew #3 locking time (HH:MM) | +| curfewUnlockTime3 | Text | The curfew #3 unlocking time (HH:MM) | +| curfewEnabled4 | Switch | Indicator if curfew #4 configuration is enabled | +| curfewLockTime4 | Text | The curfew #4 locking time (HH:MM) | +| curfewUnlockTime4 | Text | The curfew #4 unlocking time (HH:MM) | +| lockingMode | Text | The locking mode (e.g. in/out, in-only, out-only etc.) | +| online | Switch | Indicator if the flap is connected to the hub | +| lowBattery | Switch | Indicator if the battery voltage is low | +| batteryLevel | Number | The battery voltage percentage | +| batteryVoltage | Number | The absolute battery voltage measurement | +| deviceRSSI | Number | The received device signal strength in dB | +| hubRSSI | Number | The received hub signal strength in dB | + +### Feeder Device Thing + +| Channel | Type | Description | +|-------------------|-------------|-------------------------------------------------------------------------------------------------| +| id | Number | A unique id assigned by the Sure Petcare API | +| name | Text | The name of the feeder | +| product | Text | The type of product | +| online | Switch | Indicator if the feeder is connected to the hub | +| lowBattery | Switch | Indicator if the battery voltage is low | +| batteryLevel | Number | The battery voltage percentage | +| batteryVoltage | Number | The absolute battery voltage measurement | +| deviceRSSI | Number | The received device signal strength in dB | +| hubRSSI | Number | The received hub signal strength in dB | +| bowls | Text | The feeder bowls type (1 big bowl or 2 half bowls) | +| bowlsFood | Text | The feeder big bowl food type (wet food, dry food or both) | +| bowlsTarget | Number:Mass | The feeder big bowl target weight in gram (even if user setting is oz, API stores this in gram) | +| bowlsFoodLeft | Text | The feeder left half bowl food type (wet food, dry food or both) | +| bowlsTargetLeft | Number:Mass | The feeder left half bowl target weight | +| bowlsFoodRight | Text | The feeder right half bowl food type (wet food, dry food or both) | +| bowlsTargetRight | Number:Mass | The feeder right half bowl target weight | +| bowlsCloseDelay | Text | The feeder lid close delay (fast, normal, slow) | +| bowlsTrainingMode | Text | The feeder training mode (off, full open, almost full open, half closed, almost closed) | + +### Pet Thing + +| Channel | Type | Description | +|------------------------|-------------|-------------------------------------------------------------------------------------| +| id | Number | A unique id assigned by the Sure Petcare API | +| name | Text | The name of the pet | +| comment | Text | A user provided comment/description | +| gender | Text | The pet's gender | +| breed | Text | The pet's breed | +| species | Text | The pet's species | +| photo | Image | The image of the pet | +| tagIdentifier | Text | The unique identifier of the pet's micro chip or collar tag | +| location | Text | The current location of the pet (0=unknown, 1=inside, 2=outside) | +| locationChanged | DateTime | The time when the location was last changed | +| locationTimeoffset | String | Time-Command to set the pet location with a time offset. (10, 30 or 60 minutes ago) | +| locationChangedThrough | Text | The device name or username where the pet left/entered the house | +| weight | Number:Mass | The pet's weight (in kilogram) | +| dateOfBirth | DateTime | The pet's date of birth | +| feederDevice | Text | The device from which the pet last ate | +| feederLastChange | Number:Mass | The last eaten change in gram (big bowl) | +| feederLastChangeLeft | Number:Mass | The last eaten change in gram (half bowl left) | +| feederLastChangeRight | Number:Mass | The last eaten change in gram (half bowl right) | +| feederLastFeeding | DateTime | The pet's last eaten date | + +## Manual configuration + +### Things configuration + +If you use the label parameter (after the @ sign) you then have these things grouped in single tabs on PaperUI Control. + +``` +Bridge surepetcare:bridge:bridge1 "Demo API Bridge" @ "SurePetcare" [ username="", password="", refreshIntervalTopology=36000, refreshIntervalStatus=300 ] +{ + Thing household 12345 "My Household" @ "SurePetcare" + Thing hubDevice 123456 "My SurePetcare Hub" @ "SurePetcare Devices" + Thing flapDevice 123456 "My Backdoor Cat Flap" @ "SurePetcare Devices" + Thing feederDevice 123456 "My Pet Feeder" @ "SurePetcare Devices" + Thing pet 12345 "My Cat" @ "SurePetcare Pets" +} +``` + +### Items configuration + +``` +/* ***************************************** + * Bridge + * *****************************************/ +Group dgPet +Switch UR_1a_Online "Bridge Online [%s]" (dgPet) {channel="surepetcare:bridge:bridge1:online"} +Switch UR_1a_Refresh "Bridge Data Refresh [%s]" (dgPet) {channel="surepetcare:bridge:bridge1:refresh"} + +/* ***************************************** + * Household + * *****************************************/ +Number UR_1b_Id "Household Id [%d]" (dgPet) {channel="surepetcare:household:bridge1:12345:id"} +String UR_1b_Name "Household Name [%s]" (dgPet) {channel="surepetcare:household:bridge1:12345:name"} +Number UR_1b_TimezoneId "Household Timezone Id [%d]" (dgPet) {channel="surepetcare:household:bridge1:12345:timezoneId"} + +/* ***************************************** + * Hub + * *****************************************/ +Number UR_1c_Id "Hub Id [%d]" (dgPet) {channel="surepetcare:hubDevice:bridge1:123456:id"} +String UR_1c_Name "Hub Name [%s]" (dgPet) {channel="surepetcare:hubDevice:bridge1:123456:name"} +String UR_1c_Product "Hub Product [%s]" (dgPet) {channel="surepetcare:hubDevice:bridge1:123456:product"} +String UR_1c_LEDMode "Hub LED Mode [%s]" (dgPet) {channel="surepetcare:hubDevice:bridge1:123456:ledMode"} +String UR_1c_PairingMode "Hub Pairing Mode [%s]" (dgPet) {channel="surepetcare:hubDevice:bridge1:123456:pairingMode"} +Switch UR_1c_Online "Hub Online [%s]" (dgPet) {channel="surepetcare:hubDevice:bridge1:123456:online"} + +/* ***************************************** + * Cat/Pet Flap + * *****************************************/ +Number UR_1d_Id "Flap Id [%d]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:id"} +String UR_1d_Name "Flap Name [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:name"} +String UR_1d_Product "Flap Product [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:product"} +Switch UR_1d_CurfewEnabled1 "Flap Curfew 1 Enabled [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewEnabled1"} +String UR_1d_CurfewLockTime1 "Flap Curfew 1 Lock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewLockTime1"} +String UR_1d_CurfewUnlockTime1 "Flap Curfew 1 Unlock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewUnlockTime1"} +Switch UR_1d_CurfewEnabled2 "Flap Curfew 2 Enabled [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewEnabled2"} +String UR_1d_CurfewLockTime2 "Flap Curfew 2 Lock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewLockTime2"} +String UR_1d_CurfewUnlockTime2 "Flap Curfew 2 Unlock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewUnlockTime2"} +Switch UR_1d_CurfewEnabled3 "Flap Curfew 3 Enabled [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewEnabled3"} +String UR_1d_CurfewLockTime3 "Flap Curfew 3 Lock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewLockTime3"} +String UR_1d_CurfewUnlockTime3 "Flap Curfew 3 Unlock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewUnlockTime3"} +Switch UR_1d_CurfewEnabled4 "Flap Curfew 4 Enabled [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewEnabled4"} +String UR_1d_CurfewLockTime4 "Flap Curfew 4 Lock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewLockTime4"} +String UR_1d_CurfewUnlockTime5 "Flap Curfew 4 Unlock Time [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:curfewUnlockTime4"} +String UR_1d_LockingMode "Flap Locking Mode [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:lockingMode"} +Switch UR_1d_LowBattery "Flap Low Battery [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:lowBattery"} +Number UR_1d_BatteryLevel "Flap Battery Level [%.0f %%]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:batteryLevel"} +Number UR_1d_BatteryVoltage "Flap Battery Voltage [%.1f V]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:batteryVoltage"} +Switch UR_1d_Online "Flap Online [%s]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:online"} +Number UR_1d_DeviceRSSI "Flap Device RSSI [%.2f dB]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:deviceRSSI"} +Number UR_1d_HubRSSI "Flap Hub RSSI [%.2f dB]" (dgPet) {channel="surepetcare:flapDevice:bridge1:123456:hubRSSI"} + +/* ***************************************** + * Pet + * *****************************************/ +Number UR_1e_Id "Pet Id [%d]" (dgPet) {channel="surepetcare:pet:bridge1:12345:id"} +String UR_1e_Name "Pet Name [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:name"} +String UR_1e_Comment "Pet Comment [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:comment"} +String UR_1e_Gender "Pet Gender [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:gender"} +String UR_1e_Breed "Pet Breed [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:breed"} +String UR_1e_Species "Pet Species [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:species"} +Image UR_1e_Photo "Pet Photo" (dgPet) {channel="surepetcare:pet:bridge1:12345:photo"} +String UR_1e_TagIdentifier "Pet Tag Identifier [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:tagIdentifier"} +String UR_1e_Location "Pet Location [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:location"} +String UR_1e_LocationTimeoffset"Pet Switch Location [%s]" (gCats) {channel="surepetcare:pet:bridge1:20584:locationTimeoffset"} +DateTime UR_1e_LocationChanged "Pet Loc. Updated [%1$ta. %1$tH:%1$tM]" (dgPet) {channel="surepetcare:pet:bridge1:12345:locationChanged"} +String UR_1e_LocationThrough "Pet Entered / Left through [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:locationChangedThrough"} +Number:Mass UR_1e_Weight "Pet Weight [%.1f %unit%]" (dgPet) {channel="surepetcare:pet:bridge1:12345:weight"} +DateTime UR_1e_DateOfBirth "Pet Date of Birth [%1$td.%1$tm.%1$tY]" (dgPet) {channel="surepetcare:pet:bridge1:12345:dateOfBirth"} +// Pet Feeder Data +String UR_1e_Device "Device Name [%s]" (dgPet) {channel="surepetcare:pet:bridge1:12345:feederDevice"} +Number:Mass UR_1e_Change "Change: [%.2f %unit%]" (dgPet) {channel="surepetcare:pet:bridge1:12345:feederLastChange"} +Number:Mass UR_1e_ChangeLeft "Change: L [%.2f %unit%]" (dgPet) {channel="surepetcare:pet:bridge1:12345:feederLastChangeLeft"} +Number:Mass UR_1e_ChangeRight "Change: R [%.2f %unit%]" (dgPet) {channel="surepetcare:pet:bridge1:12345:feederLastChangeRight"} +DateTime UR_1e_FeedAt "Last Feeding [%1$ta. %1$tH:%1$tM]" (dgPet) {channel="surepetcare:pet:bridge1:12345:feederLastFeeding"} + +/* ***************************************** + * Pet Feeder + * *****************************************/ +Number UR_1f_Id "Feeder ID [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:id"} +String UR_1f_Name "Feeder Name [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:name"} +String UR_1f_Product "Feeder Product [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:product"} +Switch UR_1f_LowBattery "Feeder Low Battery [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:lowBattery"} +Number UR_1f_BatteryLevel "Feeder Battery Level [%.0f %%]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:batteryLevel"} +Number UR_1f_BatteryVoltage "Feeder Battery Voltage [%.2f V]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:batteryVoltage"} +String UR_1f_BowlsType "Feeder Bowls Type [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowls"} +String UR_1f_BowlsFoodtype "Feeder Food Type [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsFood"} +Number:Mass UR_1f_BowlsTarget "Feeder Target [%.0f %unit%]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsTarget"} +String UR_1f_BowlsFoodtypeLeft "Feeder Food Type L [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsFoodLeft"} +Number:Mass UR_1f_BowlsTargetLeft "Feeder Target L [%.0f %unit%]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsTargetLeft"} +String UR_1f_BowlsFoodtypeRight "Feeder Food Type R [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsFoodRight"} +Number:Mass UR_1f_BowlsTargetRight "Feeder Target R [%.0f %unit%]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsTargetRight"} +String UR_1f_BowlsLidCloseDelay "Feeder Close Delay [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsCloseDelay"} +String UR_1f_BowlsTrainingMode "Feeder Training Mode [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:bowlsTrainingMode"} +Switch UR_1f_Online "Feeder Status [%s]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:online"} +Number UR_1f_DeviceRSSI "Feeder Device Signal [%.2f dB]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:deviceRSSI"} +Number UR_1f_HubRSSI "Feeder Hub Signal [%.2f dB]" (dgPet) {channel="surepetcare:feederDevice:bridge1:123456:hubRSSI"} +``` + +### Sitemap Configuration + +``` +sitemap surepetcare label="My home automation" { + Frame label="Bridge" { + Text item=UR_1a_Online valuecolor=[ON="green", OFF="red"] + Switch item=UR_1a_Refresh + } + + Frame label="Single Pet/Cats items" { + Text item=UR_1e_Location valuecolor=[1="green", 2="red"] + // to see also the item state, just remove the brackets from the label + Switch item=UR_1e_Location label="Set Pet Location []" mappings=[1="Inside", 2="Outside"] + // Selection item=UR_1e_Location label="Set Pet Location []" mappings=[1="Im Haus", 2="Draußen"] + Text item=UR_1e_LocationChanged + Switch item=UR_1e_LocationTimeoffset label="Set Loc with time offset []" mappings=[10="-10min", 30="-30min", 60="-1h"] + Text item=UR_1e_LocationThrough + Text item=UR_1e_Id icon="text" + Text item=UR_1e_Name + Text item=UR_1e_Comment + Text item=UR_1e_Gender + Text item=UR_1e_Breed + Text item=UR_1e_Species + Text item=UR_1e_MicroChip + Text item=UR_1e_Weight icon="text" + Text item=UR_1e_DateOfBirth + Text item=UR_1e_FeedDevice + /*Text item=UR_1e_FeedChange icon="text"*/ // if you have one big bowl in your feeder use this line and comment the following 2 out + Text item=UR_1e_FeedChangeLeft icon="text" + Text item=UR_1e_FeedChangeRight icon="text" + Text item=UR_1e_FeedAt + Image item=UR_1e_Photo + } + + Frame label="Hub Device" { + Text item=UR_1c_HubOnline valuecolor=[ON="green", OFF="red"] + Text item=UR_1c_HubId icon="text" + Text item=UR_1c_HubName + Text item=UR_1c_HubProduct + Switch item=UR_1c_HubLedMode mappings=[0="Off", 1="Bright", 4="Dimmed"] + Text item=UR_1c_HubPairingMode + } + + Frame label="Flap Device" { + Text item=UR_1d_FlapOnline valuecolor=[ON="green", OFF="red"] + Text item=UR_1d_FlapId icon="text" + Text item=UR_1d_FlapName + Text item=UR_1d_FlapProduct + Switch item=UR_1d_FlapCurfewEnabled1 + Text item=UR_1d_FlapCurfewLocktime1 + Text item=UR_1d_FlapCurfewUnlocktime1 + Switch item=UR_1d_FlapCurfewEnabled2 + Text item=UR_1d_FlapCurfewLocktime2 + Text item=UR_1d_FlapCurfewUnlocktime2 + Switch item=UR_1d_FlapCurfewEnabled3 + Text item=UR_1d_FlapCurfewLocktime3 + Text item=UR_1d_FlapCurfewUnlocktime3 + Switch item=UR_1d_FlapCurfewEnabled4 + Text item=UR_1d_FlapCurfewLocktime4 + Text item=UR_1d_FlapCurfewUnlocktime4 + Text item=UR_1d_FlapLockingMode + Text item=UR_1d_FlapLowBattery valuecolor=[OFF="green", ON="red"] + Text item=UR_1d_FlapBatteryLevel icon="battery" + Text item=UR_1d_FlapBatteryVoltage icon="text" + Text item=UR_1d_FlapDeviceRSSI icon="network" + Text item=UR_1d_FlapHubRSSI icon="network" + } + + Frame label="Feeder Device" { + Text item=UR_1f_FeederOnline valuecolor=[ON="green", OFF="red"] + Text item=UR_1f_FeederId icon="text" + Text item=UR_1f_FeederName + Text item=UR_1f_FeederProduct + Text item=UR_1f_FeederLowBattery valuecolor=[OFF="green", ON="red"] + Text item=UR_1f_FeederBatteryLevel icon="battery" + Text item=UR_1f_FeederBatteryVoltage icon="text" + Text item=UR_1f_FeederBowlsType + /*Text item=UR_1f_FeederBowlsFoodtype + Text item=UR_1f_FeederBowlsTarget icon="text"*/ + Text item=UR_1f_FeederBowlsFoodtypeLeft + Text item=UR_1f_FeederBowlsTargetLeft icon="text" + Text item=UR_1f_FeederBowlsFoodtypeRight + Text item=UR_1f_FeederBowlsTargetRight icon="text" + Text item=UR_1f_FeederBowlsLidCloseDelay + Text item=UR_1f_FeederBowlsTrainingMode + Text item=UR_1f_FeederDeviceRSSI icon="network" + Text item=UR_1f_FeederHubRSSI icon="network" + } +} + +``` + +### Using Group Items + +You can also set pet locations with a group item. Please Note: the location for each pet gets updated only if the current location is not already the location you want to set. This can be very useful if you have alot of pets that often enter the home by any window/door. +Your .items file should contain this: + +``` +Group:String:OR(1,2) gLocation "Cats inside [%d]" +String UR_1e_Location "Pet Location [%s]" (dgPet, gLocation) {channel="surepetcare:pet:bridge1:12345:location"} +``` + +And your .sitemap file could look like this: + +``` +Frame label="Group Pet/Cats items" { + Selection item=gLocation label="Set ALL cats to:" mappings=[1="Inside", 2="Outside"] icon="text" + Switch item=gLocation label="Set ALL cats to: []" mappings=[1="Inside", 2="Outside"] + Group item=gLocation +} +``` + +## Troubleshooting + +| Problem | Solution | +|---------------------------------------------|-------------------------------------------------------------------------------------| +| Bridge cannot connect to Sure Petcare API | Check if you can logon to the Sure Petcare app with the given username/password. | + diff --git a/bundles/org.openhab.binding.surepetcare/pom.xml b/bundles/org.openhab.binding.surepetcare/pom.xml new file mode 100644 index 00000000000..6f10b841e35 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.surepetcare + + openHAB Add-ons :: Bundles :: SurePetcare Binding + + diff --git a/bundles/org.openhab.binding.surepetcare/src/main/feature/feature.xml b/bundles/org.openhab.binding.surepetcare/src/main/feature/feature.xml new file mode 100644 index 00000000000..f2136c4d95d --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.surepetcare/${project.version} + + diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/AuthenticationException.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/AuthenticationException.java new file mode 100644 index 00000000000..2aa14179de1 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/AuthenticationException.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AuthenticationException} is thrown if the authentication/login process is unsuccessful. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class AuthenticationException extends Exception { + + private static final long serialVersionUID = -7851429815600130535L; + + public AuthenticationException() { + super(); + } + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareAPIHelper.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareAPIHelper.java new file mode 100644 index 00000000000..3445cc9d3ee --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareAPIHelper.java @@ -0,0 +1,473 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.ProtocolException; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceControl; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfewList; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceStatus; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareLoginCredentials; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareLoginResponse; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePet; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePetStatus; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareTag; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareTopology; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link SurePetcareAPIHelper} is a helper class to abstract the Sure Petcare API. It handles authentication and + * all JSON API calls. If an API call fails it automatically refreshes the authentication token and retries. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareAPIHelper { + + private final Logger logger = LoggerFactory.getLogger(SurePetcareAPIHelper.class); + + private static final String API_USER_AGENT = "Mozilla/5.0 (Linux; Android 7.0; SM-G930F Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/64.0.3282.137 Mobile Safari/537.36"; + + private static final String API_URL = "https://app.api.surehub.io/api"; + private static final String TOPOLOGY_URL = API_URL + "/me/start"; + private static final String PET_BASE_URL = API_URL + "/pet"; + private static final String PET_STATUS_URL = API_URL + "/pet/?with[]=status&with[]=photo"; + private static final String DEVICE_BASE_URL = API_URL + "/device"; + private static final String LOGIN_URL = API_URL + "/auth/login"; + + public static final int DEFAULT_DEVICE_ID = 12344711; + + private String authenticationToken = ""; + private String username = ""; + private String password = ""; + + private @NonNullByDefault({}) HttpClient httpClient; + private SurePetcareTopology topologyCache = new SurePetcareTopology(); + + /** + * Sets the httpClient object to be used for API calls to Sure Petcare. + * + * @param httpClient the client to be used. + */ + public void setHttpClient(@Nullable HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * This method uses the provided username and password to obtain an authentication token used for subsequent API + * calls. + * + * @param username The Sure Petcare username (email address) to be used + * @param password The password + * @throws AuthenticationException + */ + public synchronized void login(String username, String password) throws AuthenticationException { + try { + Request request = httpClient.POST(LOGIN_URL); + setConnectionHeaders(request); + request.content(new StringContentProvider(SurePetcareConstants.GSON + .toJson(new SurePetcareLoginCredentials(username, password, getDeviceId().toString())))); + ContentResponse response = request.send(); + if (response.getStatus() == HttpURLConnection.HTTP_OK) { + SurePetcareLoginResponse loginResponse = SurePetcareConstants.GSON + .fromJson(response.getContentAsString(), SurePetcareLoginResponse.class); + if (loginResponse != null) { + authenticationToken = loginResponse.getToken(); + this.username = username; + this.password = password; + logger.debug("Login successful"); + } else { + throw new AuthenticationException("Invalid JSON response from login"); + } + } else { + logger.debug("HTTP Response Code: {}", response.getStatus()); + logger.debug("HTTP Response Msg: {}", response.getReason()); + throw new AuthenticationException( + "HTTP response " + response.getStatus() + " - " + response.getReason()); + } + } catch (IOException | InterruptedException | TimeoutException | ExecutionException e) { + throw new AuthenticationException(e); + } + } + + /** + * Refreshes the whole topology, i.e. all devices, pets etc. through a call to the Sure Petcare API. The APi call is + * quite resource intensive and should be used very infrequently. + */ + public synchronized void updateTopologyCache() { + try { + SurePetcareTopology tc = SurePetcareConstants.GSON.fromJson(getDataFromApi(TOPOLOGY_URL), + SurePetcareTopology.class); + if (tc != null) { + topologyCache = tc; + } + } catch (JsonSyntaxException | SurePetcareApiException e) { + logger.warn("Exception caught during topology cache update", e); + } + } + + /** + * Refreshes the pet information. This API call can be used more frequently. + * Unlike for the "position" API endpoint, there is none for the "status" (activity/feeding). + * We also dont need to specify a "petId" in the call, so we just need to call the API once. + */ + public synchronized void updatePetStatus() { + try { + String url = PET_STATUS_URL; + topologyCache.pets = Arrays + .asList(SurePetcareConstants.GSON.fromJson(getDataFromApi(url), SurePetcarePet[].class)); + } catch (JsonSyntaxException | SurePetcareApiException e) { + logger.warn("Exception caught during pet status update", e); + } + } + + /** + * Returns the whole topology. + * + * @return the topology + */ + public final SurePetcareTopology getTopology() { + return topologyCache; + } + + /** + * Returns a household object if one exists with the given id, otherwise null. + * + * @param id the household id + * @return the household with the given id + */ + public final @Nullable SurePetcareHousehold getHousehold(String id) { + return topologyCache.getById(topologyCache.households, id); + } + + /** + * Returns a device object if one exists with the given id, otherwise null. + * + * @param id the device id + * @return the device with the given id + */ + public final @Nullable SurePetcareDevice getDevice(String id) { + return topologyCache.getById(topologyCache.devices, id); + } + + /** + * Returns a pet object if one exists with the given id, otherwise null. + * + * @param id the pet id + * @return the pet with the given id + */ + public final @Nullable SurePetcarePet getPet(String id) { + return topologyCache.getById(topologyCache.pets, id); + } + + /** + * Returns a tag object if one exists with the given id, otherwise null. + * + * @param id the tag id + * @return the tag with the given id + */ + public final @Nullable SurePetcareTag getTag(String id) { + return topologyCache.getById(topologyCache.tags, id); + } + + /** + * Returns the status object if a pet exists with the given id, otherwise null. + * + * @param id the pet id + * @return the status of the pet with the given id + */ + public final @Nullable SurePetcarePetStatus getPetStatus(String id) { + SurePetcarePet pet = topologyCache.getById(topologyCache.pets, id); + return pet == null ? null : pet.status; + } + + /** + * Updates the pet location through an API call to the Sure Petcare API. + * + * @param pet the pet + * @param newLocationId the id of the new location + * @throws SurePetcareApiException + */ + public synchronized void setPetLocation(SurePetcarePet pet, Integer newLocationId, ZonedDateTime newSince) + throws SurePetcareApiException { + pet.status.activity.where = newLocationId; + pet.status.activity.since = newSince; + String url = PET_BASE_URL + "/" + pet.id.toString() + "/position"; + setDataThroughApi(url, HttpMethod.POST, pet.status.activity); + } + + /** + * Updates the device locking mode through an API call to the Sure Petcare API. + * + * @param device the device + * @param newLockingModeId the id of the new locking mode + * @throws SurePetcareApiException + */ + public synchronized void setDeviceLockingMode(SurePetcareDevice device, Integer newLockingModeId) + throws SurePetcareApiException { + // post new JSON control structure to API + SurePetcareDeviceControl control = new SurePetcareDeviceControl(); + control.lockingModeId = newLockingModeId; + String ctrlurl = DEVICE_BASE_URL + "/" + device.id.toString() + "/control"; + setDataThroughApi(ctrlurl, HttpMethod.PUT, control); + + // now we're fetching the new state back for the cache + String devurl = DEVICE_BASE_URL + "/" + device.id.toString() + "/status"; + SurePetcareDeviceStatus newStatus = SurePetcareConstants.GSON.fromJson(getDataFromApi(devurl), + SurePetcareDeviceStatus.class); + device.status.assign(newStatus); + } + + /** + * Updates the device led mode through an API call to the Sure Petcare API. + * + * @param device the device + * @param newLedModeId the id of the new led mode + * @throws SurePetcareApiException + */ + public synchronized void setDeviceLedMode(SurePetcareDevice device, Integer newLedModeId) + throws SurePetcareApiException { + // post new JSON control structure to API + SurePetcareDeviceControl control = new SurePetcareDeviceControl(); + control.ledModeId = newLedModeId; + String ctrlurl = DEVICE_BASE_URL + "/" + device.id.toString() + "/control"; + setDataThroughApi(ctrlurl, HttpMethod.PUT, control); + + // now we're fetching the new state back for the cache + String devurl = DEVICE_BASE_URL + "/" + device.id.toString() + "/status"; + SurePetcareDeviceStatus newStatus = SurePetcareConstants.GSON.fromJson(getDataFromApi(devurl), + SurePetcareDeviceStatus.class); + device.status.assign(newStatus); + } + + /** + * Updates all curfews through an API call to the Sure Petcare API. + * + * @param device the device + * @param curfewList the list of curfews + * @throws SurePetcareApiException + */ + public synchronized void setCurfews(SurePetcareDevice device, SurePetcareDeviceCurfewList curfewList) + throws SurePetcareApiException { + // post new JSON control structure to API + SurePetcareDeviceControl control = new SurePetcareDeviceControl(); + control.curfewList = curfewList.compact(); + String ctrlurl = DEVICE_BASE_URL + "/" + device.id.toString() + "/control"; + setDataThroughApi(ctrlurl, HttpMethod.PUT, control); + + // now we're fetching the new state back for the cache + String devurl = DEVICE_BASE_URL + "/" + device.id.toString() + "/control"; + SurePetcareDeviceControl newControl = SurePetcareConstants.GSON.fromJson(getDataFromApi(devurl), + SurePetcareDeviceControl.class); + if (newControl != null) { + newControl.curfewList = newControl.curfewList.order(); + } + device.control = newControl; + } + + /** + * Returns a unique device id used during the authentication process with the Sure Petcare API. The id is derived + * from the local MAC address or hostname. + * + * @return a unique device id + */ + public final Integer getDeviceId() { + try { + return getDeviceId(NetworkInterface.getNetworkInterfaces(), InetAddress.getLocalHost()); + } catch (UnknownHostException | SocketException e) { + logger.warn("unable to discover mac or hostname, assigning default device id {}", DEFAULT_DEVICE_ID); + return DEFAULT_DEVICE_ID; + } + } + + /** + * Returns a unique device id used during the authentication process with the Sure Petcare API. The id is derived + * from the local MAC address or hostname provided as arguments + * + * @param interfaces a list of interface of this host + * @param localHostAddress the ip address of the localhost + * @return a unique device id + */ + public final int getDeviceId(Enumeration interfaces, InetAddress localHostAddress) { + int decimal = DEFAULT_DEVICE_ID; + try { + if (interfaces.hasMoreElements()) { + NetworkInterface netif = interfaces.nextElement(); + + byte[] mac = netif.getHardwareAddress(); + if (mac != null) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mac.length; i++) { + sb.append(String.format("%02x", mac[i])); + } + String hex = sb.toString(); + decimal = Math.abs((int) (Long.parseUnsignedLong(hex, 16) % Integer.MAX_VALUE)); + logger.debug("current MAC address: {}, device id: {}", hex, decimal); + } else { + String hostname = localHostAddress.getHostName(); + decimal = hostname.hashCode(); + logger.debug("current hostname: {}, device id: {}", hostname, decimal); + } + } else { + String hostname = localHostAddress.getHostName(); + decimal = hostname.hashCode(); + logger.debug("current hostname: {}, device id: {}", hostname, decimal); + } + } catch (SocketException e) { + logger.debug("Socket Exception", e); + } + return decimal; + } + + /** + * Sets a set of required HTTP headers for the JSON API calls. + * + * @param request the HTTP connection + * @throws ProtocolException + */ + private void setConnectionHeaders(Request request) throws ProtocolException { + // headers + request.header(HttpHeader.ACCEPT, "application/json, text/plain, */*"); + request.header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate"); + request.header(HttpHeader.AUTHORIZATION, "Bearer " + authenticationToken); + request.header(HttpHeader.CONNECTION, "keep-alive"); + request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); + request.header(HttpHeader.USER_AGENT, API_USER_AGENT); + request.header(HttpHeader.REFERER, "https://surepetcare.io/"); + request.header("Origin", "https://surepetcare.io"); + request.header("Referer", "https://surepetcare.io"); + request.header("X-Requested-With", "com.sureflap.surepetcare"); + } + + /** + * Return the "data" element of the API result as a JsonElement. + * + * @param url The URL of the API call. + * @return The "data" element of the API result. + * @throws SurePetcareApiException + */ + private JsonElement getDataFromApi(String url) throws SurePetcareApiException { + String apiResult = getResultFromApi(url); + JsonParser parser = new JsonParser(); + JsonObject object = (JsonObject) parser.parse(apiResult); + return object.get("data"); + } + + /** + * Sends a given object as a JSON payload to the API. + * + * @param url the URL + * @param requestMethod the request method (POST, PUT etc.) + * @param payload an object used for the payload + * @throws SurePetcareApiException + */ + private void setDataThroughApi(String url, HttpMethod method, Object payload) throws SurePetcareApiException { + String jsonPayload = SurePetcareConstants.GSON.toJson(payload); + postDataThroughAPI(url, method, jsonPayload); + } + + /** + * Returns the result of a GET API call as a string. + * + * @param url the URL + * @return a JSON string with the API result + * @throws SurePetcareApiException + */ + private String getResultFromApi(String url) throws SurePetcareApiException { + Request request = httpClient.newRequest(url).method(HttpMethod.GET); + ContentResponse response = executeAPICall(request); + String responseData = response.getContentAsString(); + logger.debug("API execution successful, response: {}", responseData); + return responseData; + } + + /** + * Uses the given request method to send a JSON string to an API. + * + * @param url the URL + * @param method the required request method (POST, PUT etc.) + * @param jsonPayload the JSON string + * @throws SurePetcareApiException + */ + private void postDataThroughAPI(String url, HttpMethod method, String jsonPayload) throws SurePetcareApiException { + logger.debug("postDataThroughAPI URL: {}", url); + logger.debug("postDataThroughAPI Payload: {}", jsonPayload); + Request request = httpClient.newRequest(url).method(method); + request.content(new StringContentProvider(jsonPayload)); + executeAPICall(request); + } + + /** + * Uses the given request execute the API call. If it receives an HTTP_UNAUTHORIZED response, it will automatically + * login again and retry. + * + * @param request the Request + * @return the response from the API + * @throws SurePetcareApiException + */ + private ContentResponse executeAPICall(Request request) throws SurePetcareApiException { + int retries = 3; + while (retries > 0) { + try { + setConnectionHeaders(request); + ContentResponse response = request.send(); + if ((response.getStatus() == HttpURLConnection.HTTP_OK) + || (response.getStatus() == HttpURLConnection.HTTP_CREATED)) { + return response; + } else { + logger.debug("HTTP Response Code: {}", response.getStatus()); + logger.debug("HTTP Response Msg: {}", response.getReason()); + if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) { + // authentication token has expired, login again and retry + login(username, password); + retries--; + } else { + throw new SurePetcareApiException( + "Http error: " + response.getStatus() + " - " + response.getReason()); + } + } + } catch (AuthenticationException | InterruptedException | ExecutionException | TimeoutException + | ProtocolException e) { + throw new SurePetcareApiException("Exception caught during API execution.", e); + } + } + throw new SurePetcareApiException("Can't execute API after 3 retries"); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareApiException.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareApiException.java new file mode 100644 index 00000000000..d7996b0bb5d --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareApiException.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SurePetcareApiException} is thrown during API interactions. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareApiException extends Exception { + + private static final long serialVersionUID = -7851429815604230535L; + + public SurePetcareApiException() { + super(); + } + + public SurePetcareApiException(String message) { + super(message); + } + + public SurePetcareApiException(Throwable cause) { + super(cause); + } + + public SurePetcareApiException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareConstants.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareConstants.java new file mode 100644 index 00000000000..563f786a29c --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareConstants.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.surepetcare.internal.utils.GsonLocalDateTypeAdapter; +import org.openhab.binding.surepetcare.internal.utils.GsonLocalTimeTypeAdapter; +import org.openhab.binding.surepetcare.internal.utils.GsonZonedDateTimeTypeAdapter; +import org.openhab.core.thing.ThingTypeUID; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * The {@link SurePetcareConstants} class defines common constants, which are used across the whole binding. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareConstants { + + public static final String BINDING_ID = "surepetcare"; + + // List all Thing Type UIDs, related to the binding + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_HOUSEHOLD = new ThingTypeUID(BINDING_ID, "household"); + public static final ThingTypeUID THING_TYPE_PET = new ThingTypeUID(BINDING_ID, "pet"); + public static final ThingTypeUID THING_TYPE_HUB_DEVICE = new ThingTypeUID(BINDING_ID, "hubDevice"); + public static final ThingTypeUID THING_TYPE_FLAP_DEVICE = new ThingTypeUID(BINDING_ID, "flapDevice"); + public static final ThingTypeUID THING_TYPE_FEEDER_DEVICE = new ThingTypeUID(BINDING_ID, "feederDevice"); + + public static final Set BRIDGE_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_BRIDGE); + public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>(Arrays.asList(THING_TYPE_HOUSEHOLD, + THING_TYPE_PET, THING_TYPE_HUB_DEVICE, THING_TYPE_FLAP_DEVICE, THING_TYPE_FEEDER_DEVICE)); + + public static final long DEFAULT_REFRESH_INTERVAL_TOPOLOGY = 36000; // 10 hours + public static final long DEFAULT_REFRESH_INTERVAL_STATUS = 300; // 5 mins + + public static final String PROPERTY_NAME_ID = "id"; + + public static final int FLAP_MAX_NUMBER_OF_CURFEWS = 4; + public static final int BOWL_ID_ONE_BOWL_USED = 1; + public static final int BOWL_ID_TWO_BOWLS_USED = 4; + + public static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeAdapter(ZonedDateTime.class, new GsonZonedDateTimeTypeAdapter()) + .registerTypeAdapter(LocalDate.class, new GsonLocalDateTypeAdapter()) + .registerTypeAdapter(LocalTime.class, new GsonLocalTimeTypeAdapter()).create(); + + // Bridge Channel Names + public static final String BRIDGE_CHANNEL_REFRESH = "refresh"; + + // Household Channel Names + public static final String HOUSEHOLD_CHANNEL_ID = "id"; + public static final String HOUSEHOLD_CHANNEL_NAME = "name"; + public static final String HOUSEHOLD_CHANNEL_TIMEZONE_ID = "timezoneId"; + + // Device Channel Names + public static final String DEVICE_CHANNEL_ID = "id"; + public static final String DEVICE_CHANNEL_NAME = "name"; + public static final String DEVICE_CHANNEL_PRODUCT = "product"; + public static final String DEVICE_CHANNEL_LED_MODE = "ledMode"; + public static final String DEVICE_CHANNEL_PAIRING_MODE = "pairingMode"; + public static final String DEVICE_CHANNEL_ONLINE = "online"; + public static final String DEVICE_CHANNEL_CURFEW_BASE = "curfew"; + public static final String DEVICE_CHANNEL_CURFEW_ENABLED = DEVICE_CHANNEL_CURFEW_BASE + "Enabled"; + public static final String DEVICE_CHANNEL_CURFEW_LOCK_TIME = DEVICE_CHANNEL_CURFEW_BASE + "LockTime"; + public static final String DEVICE_CHANNEL_CURFEW_UNLOCK_TIME = DEVICE_CHANNEL_CURFEW_BASE + "UnlockTime"; + public static final String DEVICE_CHANNEL_LOCKING_MODE = "lockingMode"; + public static final String DEVICE_CHANNEL_BATTERY_VOLTAGE = "batteryVoltage"; + public static final String DEVICE_CHANNEL_BATTERY_LEVEL = "batteryLevel"; + public static final String DEVICE_CHANNEL_LOW_BATTERY = "lowBattery"; + public static final String DEVICE_CHANNEL_DEVICE_RSSI = "deviceRSSI"; + public static final String DEVICE_CHANNEL_HUB_RSSI = "hubRSSI"; + public static final String DEVICE_CHANNEL_BOWLS_FOOD = "bowlsFood"; + public static final String DEVICE_CHANNEL_BOWLS_TARGET = "bowlsTarget"; + public static final String DEVICE_CHANNEL_BOWLS_FOOD_LEFT = "bowlsFoodLeft"; + public static final String DEVICE_CHANNEL_BOWLS_TARGET_LEFT = "bowlsTargetLeft"; + public static final String DEVICE_CHANNEL_BOWLS_FOOD_RIGHT = "bowlsFoodRight"; + public static final String DEVICE_CHANNEL_BOWLS_TARGET_RIGHT = "bowlsTargetRight"; + public static final String DEVICE_CHANNEL_BOWLS = "bowls"; + public static final String DEVICE_CHANNEL_BOWLS_CLOSE_DELAY = "bowlsCloseDelay"; + public static final String DEVICE_CHANNEL_BOWLS_TRAINING_MODE = "bowlsTrainingMode"; + + // Pet Channel Names + public static final String PET_CHANNEL_ID = "id"; + public static final String PET_CHANNEL_NAME = "name"; + public static final String PET_CHANNEL_COMMENT = "comment"; + public static final String PET_CHANNEL_GENDER = "gender"; + public static final String PET_CHANNEL_BREED = "breed"; + public static final String PET_CHANNEL_SPECIES = "species"; + public static final String PET_CHANNEL_PHOTO = "photo"; + public static final String PET_CHANNEL_LOCATION = "location"; + public static final String PET_CHANNEL_LOCATION_CHANGED = "locationChanged"; + public static final String PET_CHANNEL_LOCATION_TIMEOFFSET = "locationTimeoffset"; + public static final String PET_CHANNEL_LOCATION_CHANGED_THROUGH = "locationChangedThrough"; + public static final String PET_CHANNEL_DATE_OF_BIRTH = "dateOfBirth"; + public static final String PET_CHANNEL_WEIGHT = "weight"; + public static final String PET_CHANNEL_TAG_IDENTIFIER = "tagIdentifier"; + public static final String PET_CHANNEL_FEEDER_DEVICE = "feederDevice"; + public static final String PET_CHANNEL_FEEDER_LASTFEEDING = "feederLastFeeding"; + public static final String PET_CHANNEL_FEEDER_LAST_CHANGE = "feederLastChange"; + public static final String PET_CHANNEL_FEEDER_LAST_CHANGE_LEFT = "feederLastChangeLeft"; + public static final String PET_CHANNEL_FEEDER_LAST_CHANGE_RIGHT = "feederLastChangeRight"; +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareHandlerFactory.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareHandlerFactory.java new file mode 100644 index 00000000000..546fda8112b --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/SurePetcareHandlerFactory.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.surepetcare.internal.handler.SurePetcareBridgeHandler; +import org.openhab.binding.surepetcare.internal.handler.SurePetcareDeviceHandler; +import org.openhab.binding.surepetcare.internal.handler.SurePetcareHouseholdHandler; +import org.openhab.binding.surepetcare.internal.handler.SurePetcarePetHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcareHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Rene Scherer - Initial contribution + * + */ +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.surepetcare") +@NonNullByDefault +public class SurePetcareHandlerFactory extends BaseThingHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(SurePetcareHandlerFactory.class); + + private SurePetcareAPIHelper petcareAPI = new SurePetcareAPIHelper(); + + private static final Set SUPPORTED_THING_TYPES_UIDS = Stream + .of(BRIDGE_THING_TYPES_UIDS, SurePetcareConstants.SUPPORTED_THING_TYPES_UIDS).flatMap(Collection::stream) + .collect(Collectors.toSet()); + + /** + * Returns true if the factory supports the given thing type. + */ + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + /** + * Returns a newly created thing handler for the given thing. + */ + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + logger.debug("createHandler - create handler for {}", thing); + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_HOUSEHOLD)) { + return new SurePetcareHouseholdHandler(thing, petcareAPI); + } else if (thingTypeUID.equals(THING_TYPE_HUB_DEVICE)) { + return new SurePetcareDeviceHandler(thing, petcareAPI); + } else if (thingTypeUID.equals(THING_TYPE_FLAP_DEVICE)) { + return new SurePetcareDeviceHandler(thing, petcareAPI); + } else if (thingTypeUID.equals(THING_TYPE_FEEDER_DEVICE)) { + return new SurePetcareDeviceHandler(thing, petcareAPI); + } else if (thingTypeUID.equals(THING_TYPE_PET)) { + return new SurePetcarePetHandler(thing, petcareAPI); + } else if (thingTypeUID.equals(THING_TYPE_BRIDGE)) { + return new SurePetcareBridgeHandler((Bridge) thing, petcareAPI); + } + return null; + } + + @Reference + protected void setHttpClientFactory(HttpClientFactory httpClientFactory) { + petcareAPI.setHttpClient(httpClientFactory.getCommonHttpClient()); + } + + protected void unsetHttpClientFactory(HttpClientFactory httpClientFactory) { + petcareAPI.setHttpClient(null); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/discovery/SurePetcareDiscoveryService.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/discovery/SurePetcareDiscoveryService.java new file mode 100644 index 00000000000..0de4fbde5e6 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/discovery/SurePetcareDiscoveryService.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.discovery; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +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.surepetcare.internal.dto.SurePetcareDevice; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice.ProductType; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePet; +import org.openhab.binding.surepetcare.internal.handler.SurePetcareBridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcareDiscoveryService} is an implementation of a discovery service for Sure Petcare pets and + * devices. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(SurePetcareDiscoveryService.class); + + private static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE); + + private static final int DISCOVER_TIMEOUT_SECONDS = 5; + private static final int DISCOVERY_SCAN_DELAY_MINUTES = 1; + private static final int DISCOVERY_REFRESH_INTERVAL_HOURS = 12; + + private @Nullable ScheduledFuture discoveryJob; + + private @NonNullByDefault({}) SurePetcareBridgeHandler bridgeHandler; + private @NonNullByDefault({}) ThingUID bridgeUID; + + /** + * Creates a SurePetcareDiscoveryService with enabled autostart. + */ + public SurePetcareDiscoveryService() { + super(SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS); + } + + @Override + public Set getSupportedThingTypes() { + return SUPPORTED_THING_TYPES; + } + + @Override + public void activate() { + Map properties = new HashMap<>(); + properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE); + super.activate(properties); + } + + /* We override this method to allow a call from the thing handler factory */ + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof SurePetcareBridgeHandler) { + bridgeHandler = (SurePetcareBridgeHandler) handler; + bridgeUID = bridgeHandler.getUID(); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + protected void startBackgroundDiscovery() { + logger.debug("Starting Sure Petcare household discovery"); + stopBackgroundDiscovery(); + discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, DISCOVERY_SCAN_DELAY_MINUTES, + DISCOVERY_REFRESH_INTERVAL_HOURS * 60, TimeUnit.MINUTES); + logger.debug("Scheduled topology-changed job every {} hours", DISCOVERY_REFRESH_INTERVAL_HOURS); + } + + @Override + protected void stopBackgroundDiscovery() { + ScheduledFuture job = discoveryJob; + if (job != null) { + job.cancel(true); + discoveryJob = null; + logger.debug("Stopped Sure Petcare device background discovery"); + } + } + + @Override + protected void startScan() { + logger.debug("Starting Sure Petcare discovery scan"); + // If the bridge is not online no other thing devices can be found, so no reason to scan at this moment. + removeOlderResults(getTimestampOfLastScan()); + if (bridgeHandler.getThing().getStatus() == ThingStatus.ONLINE) { + logger.debug("Starting device discovery for bridge {}", bridgeUID); + bridgeHandler.listHouseholds().forEach(this::householdDiscovered); + bridgeHandler.listPets().forEach(this::petDiscovered); + bridgeHandler.listDevices().forEach(this::deviceDiscovered); + } + } + + private void householdDiscovered(SurePetcareHousehold household) { + logger.debug("Discovered household: {}", household.name); + ThingUID thingsUID = new ThingUID(THING_TYPE_HOUSEHOLD, bridgeUID, household.id.toString()); + Map properties = new HashMap<>(household.getThingProperties()); + thingDiscovered(DiscoveryResultBuilder.create(thingsUID).withLabel(household.name).withProperties(properties) + .withRepresentationProperty(PROPERTY_NAME_ID).withBridge(bridgeUID).build()); + } + + private void petDiscovered(SurePetcarePet pet) { + logger.debug("Discovered pet: {}", pet.name); + ThingUID thingsUID = new ThingUID(THING_TYPE_PET, bridgeUID, pet.id.toString()); + Map properties = new HashMap<>(pet.getThingProperties()); + thingDiscovered(DiscoveryResultBuilder.create(thingsUID).withLabel(pet.name).withProperties(properties) + .withRepresentationProperty(PROPERTY_NAME_ID).withBridge(bridgeUID).build()); + } + + private void deviceDiscovered(SurePetcareDevice device) { + logger.debug("Discovered device: {}", device.name); + ThingTypeUID typeUID = null; + switch (ProductType.findByTypeId(device.productId)) { + case HUB: + typeUID = THING_TYPE_HUB_DEVICE; + break; + case CAT_FLAP: + typeUID = THING_TYPE_FLAP_DEVICE; + break; + case PET_FLAP: + typeUID = THING_TYPE_FLAP_DEVICE; + break; + case PET_FEEDER: + typeUID = THING_TYPE_FEEDER_DEVICE; + break; + case UNKNOWN: + default: + return; + } + ThingUID thingsUID = new ThingUID(typeUID, bridgeUID, device.id.toString()); + Map properties = new HashMap<>(device.getThingProperties()); + thingDiscovered(DiscoveryResultBuilder.create(thingsUID).withLabel(device.name).withProperties(properties) + .withRepresentationProperty(PROPERTY_NAME_ID).withBridge(bridgeUID).build()); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareBaseObject.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareBaseObject.java new file mode 100644 index 00000000000..828fcc29fea --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareBaseObject.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SurePetcareBaseObject} is the Java class used as a base DTO for other primary JSON objects. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareBaseObject { + + public Long id = 0L; + public String version = ""; + public ZonedDateTime createdAt = ZonedDateTime.now(); + public ZonedDateTime updatedAt = ZonedDateTime.now(); + + public Map getThingProperties() { + Map properties = new HashMap(); + properties.put("id", id.toString()); + properties.put("version", version); + properties.put("createdAt", createdAt.toString()); + properties.put("updatedAt", updatedAt.toString()); + return properties; + } + + public SurePetcareBaseObject assign(SurePetcareBaseObject newdev) { + this.id = newdev.id; + this.version = newdev.version; + this.createdAt = newdev.createdAt; + this.updatedAt = newdev.updatedAt; + return this; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareBridgeConfiguration.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareBridgeConfiguration.java new file mode 100644 index 00000000000..1e18a1f9650 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareBridgeConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +/** + * The {@link SurePetcareBridgeConfiguration} is a container for all the bridge configuration. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareBridgeConfiguration { + + public String username; + public String password; + public long refreshIntervalTopology = DEFAULT_REFRESH_INTERVAL_TOPOLOGY; + public long refreshIntervalStatus = DEFAULT_REFRESH_INTERVAL_STATUS; +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDevice.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDevice.java new file mode 100644 index 00000000000..9a52a7ab1c4 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDevice.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.core.thing.Thing; + +/** + * The {@link SurePetcareDevice} is the Java class used + * as a DTO to represent a Sure Petcare device, such as a hub, a cat flap, a feeder etc. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareDevice extends SurePetcareBaseObject { + + public enum ProductType { + + UNKNOWN(0, "Unknown"), + HUB(1, "Hub"), + PET_FLAP(3, "Pet Flap"), + PET_FEEDER(4, "Pet Feeder"), + CAT_FLAP(6, "Cat Flap"); + + public final Integer id; + public final String name; + + private ProductType(int id, String name) { + this.id = id; + this.name = name; + } + + public static @NonNull ProductType findByTypeId(final int id) { + return Arrays.stream(values()).filter(value -> value.id.equals(id)).findFirst().orElse(UNKNOWN); + } + } + + public Long parentDeviceId; + public Integer productId; + public Long householdId; + public String name; + public String serialNumber; + public String macAddress; + public Integer index; + public ZonedDateTime pairingAt; + public SurePetcareDeviceControl control = new SurePetcareDeviceControl(); + public SurePetcareDevice parent; + public SurePetcareDeviceStatus status = new SurePetcareDeviceStatus(); + + @Override + public @NonNull Map<@NonNull String, @NonNull String> getThingProperties() { + @NonNull + Map<@NonNull String, @NonNull String> properties = super.getThingProperties(); + properties.put("householdId", householdId.toString()); + properties.put("productType", productId.toString()); + properties.put("productName", ProductType.findByTypeId(productId).name); + properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber); + if (status.version.device != null) { + properties.put(Thing.PROPERTY_HARDWARE_VERSION, status.version.device.hardware); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, status.version.device.firmware); + } + if (status.version.lcd != null) { + properties.put(Thing.PROPERTY_HARDWARE_VERSION, status.version.lcd.hardware); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, status.version.lcd.firmware); + } + if (status.version.rf != null) { + properties.put("rfHardwareVersion", status.version.rf.hardware); + properties.put("rfFirmwareVersion", status.version.rf.firmware); + } + if (pairingAt != null) { + properties.put("pairingAt", pairingAt.toString()); + } + return properties; + } + + @Override + public String toString() { + return "Device [id=" + id + ", name=" + name + ", product=" + ProductType.findByTypeId(productId).name + "]"; + } + + public SurePetcareDevice assign(SurePetcareDevice newdev) { + super.assign(newdev); + this.parentDeviceId = newdev.parentDeviceId; + this.productId = newdev.productId; + this.householdId = newdev.householdId; + this.name = newdev.name; + this.serialNumber = newdev.serialNumber; + this.macAddress = newdev.macAddress; + this.index = newdev.index; + this.pairingAt = newdev.pairingAt; + this.control = newdev.control; + this.parent = newdev.parent; + this.status = newdev.status; + return this; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceControl.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceControl.java new file mode 100644 index 00000000000..f4d57f75da8 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceControl.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.util.List; + +import org.openhab.binding.surepetcare.internal.utils.SurePetcareDeviceCurfewListTypeAdapterFactory; + +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SurePetcareDeviceControl} class is used to serialize a JSON object to control certain parameters of a + * device (e.g. locking mode, curfew etc.). + * + * @author Rene Scherer - Initial contribution + * @author Holger Eisold - Added pet feeder status + */ +public class SurePetcareDeviceControl { + + @SerializedName("locking") + public Integer lockingModeId; + public Boolean fastPolling; + @SerializedName("led_mode") + public Integer ledModeId; + @SerializedName("pairing_mode") + public Integer pairingModeId; + public Bowls bowls; + public Lid lid; + @SerializedName("training_mode") + public Integer trainingModeId; + @SerializedName("curfew") + @JsonAdapter(SurePetcareDeviceCurfewListTypeAdapterFactory.class) + public SurePetcareDeviceCurfewList curfewList; + + public class Bowls { + public class BowlSettings { + @SerializedName("food_type") + public Integer foodId; + @SerializedName("target") + public Integer targetId; + + public BowlSettings(Integer foodId, Integer targetId) { + this.foodId = foodId; + this.targetId = targetId; + } + } + + @SerializedName("settings") + public List bowlSettings; + @SerializedName("type") + public Integer bowlId; + + public Bowls(List bowlSettings, Integer bowlId) { + this.bowlSettings = bowlSettings; + this.bowlId = bowlId; + } + } + + public class Lid { + @SerializedName("close_delay") + public Integer closeDelayId; + + public Lid(Integer closeDelayId) { + this.closeDelayId = closeDelayId; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("SurePetcareDeviceControl ["); + builder.append("lockingModeId=").append(lockingModeId); + builder.append(", fastPolling=").append(fastPolling); + builder.append(", ledModeId=").append(ledModeId); + builder.append(", pairingModeId=").append(pairingModeId); + builder.append(", trainingModeId=").append(trainingModeId); + builder.append(", curfew=").append(curfewList); + return builder.toString(); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceCurfew.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceCurfew.java new file mode 100644 index 00000000000..255513c7633 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceCurfew.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.time.LocalTime; + +/** + * The {@link SurePetcareDeviceCurfew} class is used to serialize a curfew. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareDeviceCurfew { + + public boolean enabled; + public LocalTime lockTime; + public LocalTime unlockTime; + + public SurePetcareDeviceCurfew() { + this.enabled = false; + this.lockTime = LocalTime.MIDNIGHT; + this.unlockTime = LocalTime.MIDNIGHT; + } + + public SurePetcareDeviceCurfew(boolean enabled, LocalTime lockTime, LocalTime unlockTime) { + this.enabled = enabled; + this.lockTime = lockTime; + this.unlockTime = unlockTime; + } + + @Override + public String toString() { + return enabled + "," + lockTime + "," + unlockTime; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceCurfewList.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceCurfewList.java new file mode 100644 index 00000000000..ad557a66c4c --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceCurfewList.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.util.ArrayList; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; + +/** + * The {@link SurePetcareDeviceCurfewList} class is used to serialize a list of curfew parameters. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareDeviceCurfewList extends ArrayList { + + private static final long serialVersionUID = -6947992959305282143L; + + /** + * Return the list element with the given index. If the list if too short, it will grow automatically to the given + * index. + * + * @return element with given index + */ + @Override + public SurePetcareDeviceCurfew get(int index) { + while (size() <= index) { + // grow list to required size + add(new SurePetcareDeviceCurfew()); + } + return super.get(index); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + for (SurePetcareDeviceCurfew c : this) { + builder.append("(").append(c).append(")"); + } + builder.append("]]"); + return builder.toString(); + } + + /** + * Creates a list of curfews with enabled ones at the front of the list and disabled ones at the back. + * + * @return new ordered list. + */ + public SurePetcareDeviceCurfewList order() { + SurePetcareDeviceCurfewList orderedList = new SurePetcareDeviceCurfewList(); + // remove any disabled curfews from the list + for (SurePetcareDeviceCurfew curfew : this) { + if (curfew.enabled) { + orderedList.add(curfew); + } + } + // now fill up the list with empty disabled slots. + for (int i = orderedList.size(); i < SurePetcareConstants.FLAP_MAX_NUMBER_OF_CURFEWS; i++) { + orderedList.add(new SurePetcareDeviceCurfew()); + } + return orderedList; + } + + /** + * Trims the list of curfews and removes any disabled ones. + * + * @return the new compact list of curfews. + */ + public SurePetcareDeviceCurfewList compact() { + SurePetcareDeviceCurfewList compactList = new SurePetcareDeviceCurfewList(); + // remove any disabled curfews from the list + for (SurePetcareDeviceCurfew curfew : this) { + if (curfew.enabled) { + compactList.add(curfew); + } + } + return compactList; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceStatus.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceStatus.java new file mode 100644 index 00000000000..a6076aae4e6 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareDeviceStatus.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SurePetcareDeviceStatus} class is used to serialize a JSON object to report the status of a device (e.g. + * locking mode, LED mode etc.). + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareDeviceStatus { + + @SerializedName("led_mode") + public Integer ledModeId; + @SerializedName("pairing_mode") + public Integer pairingModeId; + public Locking locking; + public Version version; + public Float battery; + // learn_mode - unknown type + public Boolean online; + public Signal signal = new Signal(); + + public SurePetcareDeviceStatus assign(SurePetcareDeviceStatus source) { + this.ledModeId = source.ledModeId; + this.pairingModeId = source.pairingModeId; + this.locking = source.locking; + this.version = source.version; + this.battery = source.battery; + this.online = source.online; + this.signal = source.signal; + return this; + } + + public class Locking { + @SerializedName("mode") + public Integer modeId; + } + + public class Version { + public class Device { + public String hardware; + public String firmware; + } + + // for Cat flaps only + public Device device = new Device(); + // for Pet flaps only + public Device lcd = new Device(); + public Device rf = new Device(); + } + + public class Signal { + public Float deviceRssi; + public Float hubRssi; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareHousehold.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareHousehold.java new file mode 100644 index 00000000000..aad89aabe2e --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareHousehold.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SurePetcareHousehold} is the Java class used as a DTO to represent a Sure Petcare Household. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareHousehold extends SurePetcareBaseObject { + + public String name; + + public String shareCode; + + public Integer timezoneId; + + @SerializedName("users") + public List users = null; + + @Override + public String toString() { + return "SurePetcareHousehold [id=" + id + ", name=" + name + "]"; + } + + public static class HouseholdUsers { + public class User { + @SerializedName("id") + public Long userId; + @SerializedName("name") + public String userName; + } + + public User user; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareLoginCredentials.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareLoginCredentials.java new file mode 100644 index 00000000000..fa9735117ef --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareLoginCredentials.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +/** + * The {@link SurePetcareLoginCredentials} is the Java class as a DTO to hold login credentials for the Sure Petcare + * API. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareLoginCredentials { + + public String emailAddress; + public String password; + public String deviceId; + + public SurePetcareLoginCredentials() { + } + + public SurePetcareLoginCredentials(String emailAddress, String password, String deviceId) { + this.emailAddress = emailAddress; + this.password = password; + this.deviceId = deviceId; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareLoginResponse.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareLoginResponse.java new file mode 100644 index 00000000000..4ff91ab3f36 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareLoginResponse.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +/** + * The {@link SurePetcareLoginResponse} is a Java class used as a DTO to hold the Sure Petcare API's login response. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareLoginResponse { + + public Data data; + + public String getToken() { + return data.token; + } + + public class Data { + + public SurePetcareUser user; + + /** + * The Sure Petcare API authentication token returned from the login call + */ + public String token; + + @Override + public String toString() { + return "Data [user=" + user + ", token=" + token + "]"; + } + } + + @Override + public String toString() { + return "SurePetcareJsonLoginResponse [data=" + data + "]"; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePet.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePet.java new file mode 100644 index 00000000000..9b1a137eb2e --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePet.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SurePetcarePet} is a DTO class used to represent a pet. It's used to deserialize JSON API results. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcarePet extends SurePetcareBaseObject { + + public enum PetGender { + + UNKNONWN(-1, "Unknown"), + FEMALE(0, "Female"), + MALE(1, "Male"); + + public final Integer id; + public final String name; + + private PetGender(int id, String name) { + this.id = id; + this.name = name; + } + + public static PetGender findByTypeId(final int id) { + return Arrays.stream(values()).filter(value -> value.id.equals(id)).findFirst().orElse(UNKNONWN); + } + } + + public enum PetSpecies { + + UNKNONWN(0, "Unknown"), + CAT(1, "Cat"), + DOG(2, "Dog"); + + public final Integer id; + public final String name; + + private PetSpecies(int id, String name) { + this.id = id; + this.name = name; + } + + public static PetSpecies findByTypeId(final int id) { + return Arrays.stream(values()).filter(value -> value.id.equals(id)).findFirst().orElse(UNKNONWN); + } + } + + public String name = ""; + + @SerializedName("gender") + public Integer genderId; + public LocalDate dateOfBirth; + public BigDecimal weight; + public String comments; + public Long householdId; + public Integer breedId; + public Long photoId; + public Integer speciesId; + public Long tagId; + public SurePetcarePhoto photo; + + public SurePetcarePetStatus status = new SurePetcarePetStatus(); + + @Override + public String toString() { + return "Pet [id=" + id + ", name=" + name + ", tagId=" + tagId + "]"; + } + + @Override + public @NonNull Map<@NonNull String, @NonNull String> getThingProperties() { + @NonNull + Map<@NonNull String, @NonNull String> properties = super.getThingProperties(); + properties.put("householdId", householdId.toString()); + return properties; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetActivity.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetActivity.java new file mode 100644 index 00000000000..619c8a1b974 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetActivity.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.time.ZonedDateTime; + +/** + * The {@link SurePetcarePetActivity} is the Java class used to represent the + * status of a pet. It's used to deserialize JSON API results. + * + * @author Rene Scherer - Initial contribution + * @author Holger Eisold - Added pet feeder status + */ +public class SurePetcarePetActivity { + + public Long tagId; + public Long deviceId; + public Long userId; + public Integer where; + public ZonedDateTime since; + + public SurePetcarePetActivity() { + } + + public SurePetcarePetActivity(Integer location, ZonedDateTime since) { + this.where = location; + this.since = since; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetFeeding.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetFeeding.java new file mode 100644 index 00000000000..ead36ee7969 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetFeeding.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SurePetcarePetFeeding} is the Java class used to represent the + * status of a pet. It's used to deserialize JSON API results. + * + * @author Rene Scherer - Initial contribution + * @author Holger Eisold - Added pet feeder status + */ +public class SurePetcarePetFeeding { + + public Long tagId; + public Long deviceId; + @SerializedName("change") + public List feedChange = new ArrayList<>(); + @SerializedName("at") + public ZonedDateTime feedChangeAt; +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetStatus.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetStatus.java new file mode 100644 index 00000000000..7f5f89d2209 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePetStatus.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +/** + * The {@link SurePetcarePetStatus} is the Java class used to represent the + * status of a pet. It's used to deserialize JSON API results. + * + * @author Rene Scherer - Initial contribution + * @author Holger Eisold - Added pet feeder status + */ +public class SurePetcarePetStatus { + + public SurePetcarePetActivity activity; + public SurePetcarePetFeeding feeding; +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePhoto.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePhoto.java new file mode 100644 index 00000000000..d79f909f903 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcarePhoto.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +/** + * The {@link SurePetcarePhoto} is the Java class used to represent the photo of a pet or user. It's used to deserialize + * JSON API results. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcarePhoto extends SurePetcareBaseObject { + + // { + // "id":78634, + // "location":"https:\/\/surehub.s3.amazonaws.com\/user-photos\/thm\/23421\/z70LUtqaHVlAIkgYRDIooi5666GvQwdAZptCgeZU.jpg", + // "uploading_user_id":34542, + // "version":"MA==", + // "created_at":"2019-09-02T09:31:07+00:00", + // "updated_at":"2019-09-02T09:31:07+00:00" + // } + + public String location; + public Long uploadingUserId; +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareTag.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareTag.java new file mode 100644 index 00000000000..ceda9d77dd9 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareTag.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link SurePetcareTag} is the Java class used to represent the micro chip or collar tag of a pet. It's used to + * deserialize JSON API results. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareTag extends SurePetcareBaseObject { + + // { + // "id":34552, + // "tag":"981.000007623719", + // "version":"MA==", + // "created_at":"2019-09-02T09:27:17+00:00", + // "updated_at":"2019-09-02T09:27:17+00:00", + // "supported_product_ids":[ + // 3, + // 4, + // 6 + // ] + // } + + public String tag; + public List supportedProductIds = new ArrayList<>(); +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareTopology.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareTopology.java new file mode 100644 index 00000000000..b8ba7af9579 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareTopology.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SurePetcareTopology} is the Java class used to represent a whole Sure Petcare topology. It's used to + * deserialize JSON API results. + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareTopology { + + public List devices = new ArrayList<>(); + public List households = new ArrayList<>(); + public List pets = new ArrayList<>(); + public List photos = new ArrayList<>(); + public List tags = new ArrayList<>(); + @Nullable + public SurePetcareUser user; + + public @Nullable T getById(List elements, String id) { + for (T e : elements) { + if (id.equals(e.id.toString())) { + return e; + } + } + return null; + } + + @Override + public String toString() { + return "SurePetcareTopology [# of Devices=" + devices.size() + ", # of Households=" + households.size() + + ", # of pets=" + pets.size() + "]"; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareUser.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareUser.java new file mode 100644 index 00000000000..741edffb933 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/dto/SurePetcareUser.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.dto; + +/** + * The {@link SurePetcareUser} is the Java class used to represent a Sure Petcare API user. It's used to deserialize + * JSON API results. + * + * @author Rene Scherer - Initial contribution + */ +public class SurePetcareUser extends SurePetcareBaseObject { + + // "user":{ + // "id":23465, + // "email_address":"rs@gugus.com", + // "first_name":"Admin", + // "last_name":"User", + // "country_id":77, + // "language_id":37, + // "marketing_opt_in":false, + // "terms_accepted":true, + // "weight_units":0, + // "time_format":0, + // "version":"MA==", + // "created_at":"2019-09-02T08:20:03+00:00", + // "updated_at":"2019-09-02T08:20:03+00:00", + // "notifications":{ + // "device_status":true, + // "animal_movement":true, + // "intruder_movements":true, + // "new_device_pet":true, + // "household_management":true, + // "photos":true, + // "low_battery":true, + // "curfew":true, + // "feeding_activity":true + // } + // } + + public String emailAddress; + public String firstName; + public String lastName; + public Integer countryId; + public Integer languageId; + // and various others not yet mapped + + @Override + public String toString() { + return "User [id=" + id + ", email_address=" + emailAddress + ", first_name=" + firstName + ", last_name=" + + lastName + "]"; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareBaseObjectHandler.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareBaseObjectHandler.java new file mode 100644 index 00000000000..70f93b0643d --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareBaseObjectHandler.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.handler; + +import static org.openhab.core.thing.ThingStatus.ONLINE; + +import java.time.Duration; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.surepetcare.internal.SurePetcareAPIHelper; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcareBaseObjectHandler} is responsible for handling the things created to represent Sure Petcare + * objects. + * + * @author Rene Scherer - Initial Contribution + */ +@NonNullByDefault +public abstract class SurePetcareBaseObjectHandler extends BaseThingHandler { + + private static final int CACHE_TIMEOUT_SECOND = 3; + + private final Logger logger = LoggerFactory.getLogger(SurePetcareBaseObjectHandler.class); + + protected final SurePetcareAPIHelper petcareAPI; + + protected ExpiringCache updateThingCache = new ExpiringCache<>(Duration.ofSeconds(CACHE_TIMEOUT_SECOND), + this::refreshCache); + + public SurePetcareBaseObjectHandler(Thing thing, final SurePetcareAPIHelper petcareAPI) { + super(thing); + this.petcareAPI = petcareAPI; + } + + @Override + public void initialize() { + updateStatus(ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateThingCache.getValue(); + } + } + + @Override + public void updateProperties(Map properties) { + super.updateProperties(properties); + } + + private @Nullable Integer refreshCache() { + logger.debug("cache has timed out, we refresh the values in the thing"); + updateThing(); + // we don't care about the cache content, we just return an empty object + return Integer.MIN_VALUE; + } + + /** + * Updates all channels of a thing. + */ + protected abstract void updateThing(); +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareBridgeHandler.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareBridgeHandler.java new file mode 100644 index 00000000000..d92b57fe2b1 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareBridgeHandler.java @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.handler; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +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.surepetcare.internal.AuthenticationException; +import org.openhab.binding.surepetcare.internal.SurePetcareAPIHelper; +import org.openhab.binding.surepetcare.internal.discovery.SurePetcareDiscoveryService; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareBridgeConfiguration; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePet; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcareBridgeHandler} is responsible for handling the bridge things created to use the Sure Petcare + * API. This way, the user credentials may be entered only once. + * + * It also spawns 2 background polling threads to update the more static data (topology) and the pet locations at + * different time intervals. + * + * @author Rene Scherer - Initial Contribution + */ +@NonNullByDefault +public class SurePetcareBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(SurePetcareBridgeHandler.class); + + private final SurePetcareAPIHelper petcareAPI; + private @Nullable ScheduledFuture topologyPollingJob; + private @Nullable ScheduledFuture petStatusPollingJob; + + public SurePetcareBridgeHandler(Bridge bridge, SurePetcareAPIHelper petcareAPI) { + super(bridge); + this.petcareAPI = petcareAPI; + } + + @Override + public void initialize() { + logger.debug("Initializing Sure Petcare bridge handler."); + SurePetcareBridgeConfiguration config = getConfigAs(SurePetcareBridgeConfiguration.class); + + if (config.username != null && config.password != null) { + updateStatus(ThingStatus.UNKNOWN); + try { + logger.debug("Login to SurePetcare API with username: {}", config.username); + petcareAPI.login(config.username, config.password); + logger.debug("Login successful, updating topology cache"); + petcareAPI.updateTopologyCache(); + logger.debug("Cache update successful, setting bridge status to ONLINE"); + updateStatus(ThingStatus.ONLINE); + updateThings(); + } catch (AuthenticationException e) { + logger.debug("Authentication exception during initializing", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-authentication"); + return; + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-missing-username-or-password"); + return; + } + + ScheduledFuture job = topologyPollingJob; + if (job == null || job.isCancelled()) { + topologyPollingJob = scheduler.scheduleWithFixedDelay(() -> { + petcareAPI.updateTopologyCache(); + updateThings(); + }, config.refreshIntervalTopology, config.refreshIntervalTopology, TimeUnit.SECONDS); + logger.debug("Bridge topology polling job every {} seconds", config.refreshIntervalTopology); + } + + job = petStatusPollingJob; + if (job == null || job.isCancelled()) { + petStatusPollingJob = scheduler.scheduleWithFixedDelay(this::pollAndUpdatePetStatus, + config.refreshIntervalStatus, config.refreshIntervalStatus, TimeUnit.SECONDS); + logger.debug("Pet status polling job every {} seconds", config.refreshIntervalStatus); + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(SurePetcareDiscoveryService.class); + } + + @Override + public void dispose() { + ScheduledFuture job = topologyPollingJob; + if (job != null && !job.isCancelled()) { + job.cancel(true); + topologyPollingJob = null; + logger.debug("Stopped topology background polling process"); + } + job = petStatusPollingJob; + if (job != null && !job.isCancelled()) { + job.cancel(true); + petStatusPollingJob = null; + logger.debug("Stopped pet status background polling process"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateState(BRIDGE_CHANNEL_REFRESH, OnOffType.OFF); + } else { + switch (channelUID.getId()) { + case BRIDGE_CHANNEL_REFRESH: + if (OnOffType.ON.equals(command)) { + petcareAPI.updateTopologyCache(); + updateThings(); + updateState(BRIDGE_CHANNEL_REFRESH, OnOffType.OFF); + } + break; + } + } + } + + public ThingUID getUID() { + return thing.getUID(); + } + + public Iterable listHouseholds() { + return petcareAPI.getTopology().households; + } + + public Iterable listPets() { + return petcareAPI.getTopology().pets; + } + + public Iterable listDevices() { + return petcareAPI.getTopology().devices; + } + + protected synchronized void updateThings() { + logger.debug("Updating {} connected things", getThing().getThings().size()); + // update existing things + for (Thing th : getThing().getThings()) { + String tid = th.getUID().getId(); + Map properties = null; + ThingHandler handler = th.getHandler(); + if (handler instanceof SurePetcarePetHandler) { + ((SurePetcarePetHandler) handler).updateThing(); + SurePetcarePet pet = petcareAPI.getTopology().getById(petcareAPI.getTopology().pets, tid); + if (pet != null) { + properties = pet.getThingProperties(); + } + } else if (handler instanceof SurePetcareHouseholdHandler) { + ((SurePetcareHouseholdHandler) handler).updateThing(); + SurePetcareHousehold household = petcareAPI.getTopology().getById(petcareAPI.getTopology().households, + tid); + if (household != null) { + properties = household.getThingProperties(); + } + } else if (handler instanceof SurePetcareDeviceHandler) { + ((SurePetcareDeviceHandler) handler).updateThing(); + SurePetcareDevice device = petcareAPI.getTopology().getById(petcareAPI.getTopology().devices, tid); + if (device != null) { + properties = device.getThingProperties(); + } + } + if ((properties != null) && (handler instanceof SurePetcareBaseObjectHandler)) { + ((SurePetcareBaseObjectHandler) handler).updateProperties(properties); + } + } + } + + private synchronized void pollAndUpdatePetStatus() { + petcareAPI.updatePetStatus(); + for (Thing th : getThing().getThings()) { + if (th.getThingTypeUID().equals(THING_TYPE_PET)) { + ThingHandler handler = th.getHandler(); + if (handler != null) { + ((SurePetcarePetHandler) handler).updateThing(); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareDeviceHandler.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareDeviceHandler.java new file mode 100644 index 00000000000..42ec95e69d7 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareDeviceHandler.java @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.handler; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +import javax.measure.quantity.Mass; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.surepetcare.internal.SurePetcareAPIHelper; +import org.openhab.binding.surepetcare.internal.SurePetcareApiException; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceControl; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceControl.Bowls.BowlSettings; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfew; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcareDeviceHandler} is responsible for handling hubs and pet flaps created to represent Sure Petcare + * devices. + * + * @author Rene Scherer - Initial Contribution + * @author Holger Eisold - Added pet feeder status + */ +@NonNullByDefault +public class SurePetcareDeviceHandler extends SurePetcareBaseObjectHandler { + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + private static final float BATTERY_FULL_VOLTAGE = 4 * 1.5f; // 4x AA batteries of 1.5V each + private static final float BATTERY_EMPTY_VOLTAGE = 4.2f; // 4x AA batteries of 1.05V each + private static final float LOW_BATTERY_THRESHOLD = 4 * 1.1f; + + private final Logger logger = LoggerFactory.getLogger(SurePetcareDeviceHandler.class); + + public SurePetcareDeviceHandler(Thing thing, SurePetcareAPIHelper petcareAPI) { + super(thing, petcareAPI); + logger.debug("Created device handler for type {}", thing.getThingTypeUID().getAsString()); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateThingCache.getValue(); + } else if (channelUID.getId().startsWith(DEVICE_CHANNEL_CURFEW_BASE)) { + handleCurfewCommand(channelUID, command); + } else { + switch (channelUID.getId()) { + case DEVICE_CHANNEL_LOCKING_MODE: + if (command instanceof StringType) { + synchronized (petcareAPI) { + SurePetcareDevice device = petcareAPI.getDevice(thing.getUID().getId()); + if (device != null) { + String newLockingModeIdStr = ((StringType) command).toString(); + try { + Integer newLockingModeId = Integer.valueOf(newLockingModeIdStr); + petcareAPI.setDeviceLockingMode(device, newLockingModeId); + updateState(DEVICE_CHANNEL_LOCKING_MODE, + new StringType(device.status.locking.modeId.toString())); + } catch (NumberFormatException e) { + logger.warn("Invalid locking mode: {}, ignoring command", newLockingModeIdStr); + } catch (SurePetcareApiException e) { + logger.warn( + "Error from SurePetcare API. Can't update locking mode {} for device {}", + newLockingModeIdStr, device, e); + } + } + } + } + break; + case DEVICE_CHANNEL_LED_MODE: + if (command instanceof StringType) { + synchronized (petcareAPI) { + SurePetcareDevice device = petcareAPI.getDevice(thing.getUID().getId()); + if (device != null) { + String newLedModeIdStr = command.toString(); + try { + Integer newLedModeId = Integer.valueOf(newLedModeIdStr); + petcareAPI.setDeviceLedMode(device, newLedModeId); + updateState(DEVICE_CHANNEL_LOCKING_MODE, + new StringType(device.status.ledModeId.toString())); + } catch (NumberFormatException e) { + logger.warn("Invalid locking mode: {}, ignoring command", newLedModeIdStr); + } catch (SurePetcareApiException e) { + logger.warn("Error from SurePetcare API. Can't update LED mode {} for device {}", + newLedModeIdStr, device, e); + } + } + } + } + break; + default: + logger.warn("Update on unsupported channel {}, ignoring command", channelUID.getId()); + } + } + } + + @Override + protected void updateThing() { + SurePetcareDevice device = petcareAPI.getDevice(thing.getUID().getId()); + if (device != null) { + logger.debug("Updating all thing channels for device : {}", device); + updateState(DEVICE_CHANNEL_ID, new DecimalType(device.id)); + updateState(DEVICE_CHANNEL_NAME, new StringType(device.name)); + updateState(DEVICE_CHANNEL_PRODUCT, new StringType(device.productId.toString())); + updateState(DEVICE_CHANNEL_ONLINE, OnOffType.from(device.status.online)); + + if (thing.getThingTypeUID().equals(THING_TYPE_HUB_DEVICE)) { + updateState(DEVICE_CHANNEL_LED_MODE, new StringType(device.status.ledModeId.toString())); + updateState(DEVICE_CHANNEL_PAIRING_MODE, new StringType(device.status.pairingModeId.toString())); + } else { + float batVol = device.status.battery; + updateState(DEVICE_CHANNEL_BATTERY_VOLTAGE, new DecimalType(batVol)); + updateState(DEVICE_CHANNEL_BATTERY_LEVEL, new DecimalType(Math.min( + (batVol - BATTERY_EMPTY_VOLTAGE) / (BATTERY_FULL_VOLTAGE - BATTERY_EMPTY_VOLTAGE) * 100.0f, + 100.0f))); + updateState(DEVICE_CHANNEL_LOW_BATTERY, OnOffType.from(batVol < LOW_BATTERY_THRESHOLD)); + updateState(DEVICE_CHANNEL_DEVICE_RSSI, new DecimalType(device.status.signal.deviceRssi)); + updateState(DEVICE_CHANNEL_HUB_RSSI, new DecimalType(device.status.signal.hubRssi)); + + if (thing.getThingTypeUID().equals(THING_TYPE_FLAP_DEVICE)) { + updateThingCurfews(device); + updateState(DEVICE_CHANNEL_LOCKING_MODE, new StringType(device.status.locking.modeId.toString())); + } else if (thing.getThingTypeUID().equals(THING_TYPE_FEEDER_DEVICE)) { + int bowlId = device.control.bowls.bowlId; + List bowlSettings = device.control.bowls.bowlSettings; + int numBowls = bowlSettings.size(); + if (numBowls > 0) { + if (bowlId == BOWL_ID_ONE_BOWL_USED) { + updateState(DEVICE_CHANNEL_BOWLS_FOOD, + new StringType(bowlSettings.get(0).foodId.toString())); + updateState(DEVICE_CHANNEL_BOWLS_TARGET, new QuantityType( + device.control.bowls.bowlSettings.get(0).targetId, SIUnits.GRAM)); + } else if (bowlId == BOWL_ID_TWO_BOWLS_USED) { + updateState(DEVICE_CHANNEL_BOWLS_FOOD_LEFT, + new StringType(bowlSettings.get(0).foodId.toString())); + updateState(DEVICE_CHANNEL_BOWLS_TARGET_LEFT, + new QuantityType(bowlSettings.get(0).targetId, SIUnits.GRAM)); + if (numBowls > 1) { + updateState(DEVICE_CHANNEL_BOWLS_FOOD_RIGHT, + new StringType(bowlSettings.get(1).foodId.toString())); + updateState(DEVICE_CHANNEL_BOWLS_TARGET_RIGHT, + new QuantityType(bowlSettings.get(1).targetId, SIUnits.GRAM)); + } + } + } + updateState(DEVICE_CHANNEL_BOWLS, new StringType(device.control.bowls.bowlId.toString())); + updateState(DEVICE_CHANNEL_BOWLS_CLOSE_DELAY, + new StringType(device.control.lid.closeDelayId.toString())); + updateState(DEVICE_CHANNEL_BOWLS_TRAINING_MODE, + new StringType(device.control.trainingModeId.toString())); + } else { + logger.warn("Unknown product type for device {}", thing.getUID().getAsString()); + } + } + } + } + + private void updateThingCurfews(SurePetcareDevice device) { + for (int i = 0; i < FLAP_MAX_NUMBER_OF_CURFEWS; i++) { + SurePetcareDeviceCurfew curfew = device.control.curfewList.get(i); + logger.debug("updateThingCurfews - Updating device curfew: {}", curfew.toString()); + updateState(DEVICE_CHANNEL_CURFEW_ENABLED + (i + 1), OnOffType.from(curfew.enabled)); + updateState(DEVICE_CHANNEL_CURFEW_LOCK_TIME + (i + 1), + new StringType(TIME_FORMATTER.format(curfew.lockTime))); + updateState(DEVICE_CHANNEL_CURFEW_UNLOCK_TIME + (i + 1), + new StringType(TIME_FORMATTER.format(curfew.unlockTime))); + } + } + + private void handleCurfewCommand(ChannelUID channelUID, Command command) { + String channelUIDBase = channelUID.getIdWithoutGroup().substring(0, + channelUID.getIdWithoutGroup().length() - 1); + int slot = Integer.parseInt(channelUID.getAsString().substring(channelUID.getAsString().length() - 1)); + + synchronized (petcareAPI) { + SurePetcareDevice device = petcareAPI.getDevice(thing.getUID().getId()); + if (device != null) { + try { + SurePetcareDeviceControl existingControl = device.control; + SurePetcareDeviceCurfew curfew = existingControl.curfewList.get(slot - 1); + boolean requiresUpdate = false; + switch (channelUIDBase) { + case DEVICE_CHANNEL_CURFEW_ENABLED: + if (command instanceof OnOffType) { + if (curfew.enabled != command.equals(OnOffType.ON)) { + logger.debug("Enabling curfew slot: {}", slot); + requiresUpdate = true; + } + curfew.enabled = (command.equals(OnOffType.ON)); + } + break; + case DEVICE_CHANNEL_CURFEW_LOCK_TIME: + LocalTime newLockTime = LocalTime.parse(command.toString(), TIME_FORMATTER); + if (!(curfew.lockTime.equals(newLockTime)) && curfew.enabled) { + logger.debug("Changing curfew slot {} lock time to: {}", slot, newLockTime); + requiresUpdate = true; + } + curfew.lockTime = newLockTime; + + break; + case DEVICE_CHANNEL_CURFEW_UNLOCK_TIME: + LocalTime newUnlockTime = LocalTime.parse(command.toString(), TIME_FORMATTER); + if (!(curfew.unlockTime.equals(newUnlockTime)) && curfew.enabled) { + logger.debug("Changing curfew slot {} unlock time to: {}", slot, newUnlockTime); + requiresUpdate = true; + } + curfew.unlockTime = newUnlockTime; + + break; + default: + break; + } + + if (requiresUpdate) { + try { + logger.debug("Updating curfews: {}", existingControl.curfewList.toString()); + petcareAPI.setCurfews(device, existingControl.curfewList); + updateThingCurfews(device); + } catch (SurePetcareApiException e) { + logger.warn("Error from SurePetcare API. Can't update curfews for device {}: {}", device, + e.getMessage()); + } + } + } catch (DateTimeParseException e) { + logger.warn("Incorrect curfew time format HH:mm: {}", e.getMessage()); + } + } + + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareHouseholdHandler.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareHouseholdHandler.java new file mode 100644 index 00000000000..4a34e33cbea --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareHouseholdHandler.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.handler; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.surepetcare.internal.SurePetcareAPIHelper; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Thing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcareHouseholdHandler} is responsible for handling things created to represent Sure Petcare + * households. + * + * @author Rene Scherer - Initial Contribution + */ +@NonNullByDefault +public class SurePetcareHouseholdHandler extends SurePetcareBaseObjectHandler { + + private final Logger logger = LoggerFactory.getLogger(SurePetcareHouseholdHandler.class); + + public SurePetcareHouseholdHandler(Thing thing, SurePetcareAPIHelper petcareAPI) { + super(thing, petcareAPI); + } + + @Override + protected void updateThing() { + SurePetcareHousehold household = petcareAPI.getHousehold(thing.getUID().getId()); + if (household != null) { + logger.debug("Updating all thing channels for household : {}", household); + updateState(HOUSEHOLD_CHANNEL_ID, new DecimalType(household.id)); + updateState(HOUSEHOLD_CHANNEL_NAME, new StringType(household.name)); + updateState(HOUSEHOLD_CHANNEL_TIMEZONE_ID, new DecimalType(household.timezoneId)); + } else { + logger.debug("Trying to update unknown household: {}", thing.getUID().getId()); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcarePetHandler.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcarePetHandler.java new file mode 100644 index 00000000000..ec1fa5b67f0 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/handler/SurePetcarePetHandler.java @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.handler; + +import static org.openhab.binding.surepetcare.internal.SurePetcareConstants.*; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import javax.measure.quantity.Mass; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.surepetcare.internal.SurePetcareAPIHelper; +import org.openhab.binding.surepetcare.internal.SurePetcareApiException; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePet; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePetActivity; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePetFeeding; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareTag; +import org.openhab.core.cache.ByteArrayFileCache; +import org.openhab.core.io.net.http.HttpUtil; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +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 org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SurePetcarePetHandler} is responsible for handling the things created to represent Sure Petcare pets. + * + * @author Rene Scherer - Initial Contribution + * @author Holger Eisold - Added pet feeder status, location time offset + */ +@NonNullByDefault +public class SurePetcarePetHandler extends SurePetcareBaseObjectHandler { + + private final Logger logger = LoggerFactory.getLogger(SurePetcarePetHandler.class); + + private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.surepetcare"); + + public SurePetcarePetHandler(Thing thing, SurePetcareAPIHelper petcareAPI) { + super(thing, petcareAPI); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateThingCache.getValue(); + } else { + switch (channelUID.getId()) { + case PET_CHANNEL_LOCATION: + logger.debug("Received location update command: {}", command.toString()); + if (command instanceof StringType) { + synchronized (petcareAPI) { + SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId()); + if (pet != null) { + String newLocationIdStr = ((StringType) command).toString(); + try { + Integer newLocationId = Integer.valueOf(newLocationIdStr); + // Only update if location has changed. (Needed for Group:Switch item) + if ((pet.status.activity.where.equals(newLocationId)) || newLocationId.equals(0)) { + logger.debug("Location has not changed, skip pet id: {} with loc id: {}", + pet.id, newLocationId); + } else { + logger.debug("Received new location: {}", newLocationId); + petcareAPI.setPetLocation(pet, newLocationId, ZonedDateTime.now()); + updateState(PET_CHANNEL_LOCATION, + new StringType(pet.status.activity.where.toString())); + updateState(PET_CHANNEL_LOCATION_CHANGED, + new DateTimeType(pet.status.activity.since)); + } + } catch (NumberFormatException e) { + logger.warn("Invalid location id: {}, ignoring command", newLocationIdStr, e); + } catch (SurePetcareApiException e) { + logger.warn("Error from SurePetcare API. Can't update location {} for pet {}", + newLocationIdStr, pet, e); + } + } + } + } + break; + case PET_CHANNEL_LOCATION_TIMEOFFSET: + logger.debug("Received location time offset update command: {}", command.toString()); + if (command instanceof StringType) { + synchronized (petcareAPI) { + SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId()); + if (pet != null) { + String commandIdStr = ((StringType) command).toString(); + try { + Integer commandId = Integer.valueOf(commandIdStr); + Integer currentLocation = pet.status.activity.where; + logger.debug("Received new location: {}", currentLocation == 1 ? 2 : 1); + // We set the location to the opposite state. + // We also set location to INSIDE (1) if currentLocation is Unknown (0) + if (commandId == 10 || commandId == 30 || commandId == 60) { + ZonedDateTime time = ZonedDateTime.now().minusMinutes(commandId); + petcareAPI.setPetLocation(pet, currentLocation == 1 ? 2 : 1, time); + + } + updateState(PET_CHANNEL_LOCATION, + new StringType(pet.status.activity.where.toString())); + updateState(PET_CHANNEL_LOCATION_CHANGED, + new DateTimeType(pet.status.activity.since)); + updateState(PET_CHANNEL_LOCATION_TIMEOFFSET, UnDefType.UNDEF); + } catch (NumberFormatException e) { + logger.warn("Invalid location id: {}, ignoring command", commandIdStr, e); + } catch (SurePetcareApiException e) { + logger.warn("Error from SurePetcare API. Can't update location {} for pet {}", + commandIdStr, pet, e); + } + } + } + } + break; + default: + logger.warn("Update on unsupported channel {}", channelUID.getId()); + } + } + } + + @Override + protected void updateThing() { + synchronized (petcareAPI) { + SurePetcarePet pet = petcareAPI.getPet(thing.getUID().getId()); + if (pet != null) { + logger.debug("Updating all thing channels for pet : {}", pet); + updateState(PET_CHANNEL_ID, new DecimalType(pet.id)); + updateState(PET_CHANNEL_NAME, pet.name == null ? UnDefType.UNDEF : new StringType(pet.name)); + updateState(PET_CHANNEL_COMMENT, pet.comments == null ? UnDefType.UNDEF : new StringType(pet.comments)); + updateState(PET_CHANNEL_GENDER, + pet.genderId == null ? UnDefType.UNDEF : new StringType(pet.genderId.toString())); + updateState(PET_CHANNEL_BREED, + pet.breedId == null ? UnDefType.UNDEF : new StringType(pet.breedId.toString())); + updateState(PET_CHANNEL_SPECIES, + pet.speciesId == null ? UnDefType.UNDEF : new StringType(pet.speciesId.toString())); + updateState(PET_CHANNEL_PHOTO, + pet.photo == null ? UnDefType.UNDEF : getPetPhotoFromCache(pet.photo.location)); + + SurePetcarePetActivity loc = pet.status.activity; + if (loc != null) { + updateState(PET_CHANNEL_LOCATION, new StringType(loc.where.toString())); + if (loc.since != null) { + updateState(PET_CHANNEL_LOCATION_CHANGED, new DateTimeType(loc.since)); + } + } + updateState(PET_CHANNEL_DATE_OF_BIRTH, pet.dateOfBirth == null ? UnDefType.UNDEF + : new DateTimeType(pet.dateOfBirth.atStartOfDay(ZoneId.systemDefault()))); + updateState(PET_CHANNEL_WEIGHT, + pet.weight == null ? UnDefType.UNDEF : new QuantityType(pet.weight, SIUnits.KILOGRAM)); + if (pet.tagId != null) { + SurePetcareTag tag = petcareAPI.getTag(pet.tagId.toString()); + if (tag != null) { + updateState(PET_CHANNEL_TAG_IDENTIFIER, new StringType(tag.tag)); + } + } + if (pet.status.activity.deviceId != null) { + SurePetcareDevice device = petcareAPI.getDevice(pet.status.activity.deviceId.toString()); + if (device != null) { + updateState(PET_CHANNEL_LOCATION_CHANGED_THROUGH, new StringType(device.name)); + } + } else if (pet.status.activity.userId != null) { + SurePetcareHousehold household = petcareAPI.getHousehold(pet.householdId.toString()); + if (household != null) { + Long userId = pet.status.activity.userId; + household.users.stream().map(user -> user.user).filter(user -> userId.equals(user.userId)) + .forEach(user -> updateState(PET_CHANNEL_LOCATION_CHANGED_THROUGH, + new StringType(user.userName))); + } + } + SurePetcarePetFeeding feeding = pet.status.feeding; + if (feeding != null) { + SurePetcareDevice device = petcareAPI.getDevice(feeding.deviceId.toString()); + if (device != null) { + updateState(PET_CHANNEL_FEEDER_DEVICE, new StringType(device.name)); + int bowlId = device.control.bowls.bowlId; + int numBowls = feeding.feedChange.size(); + if (numBowls > 0) { + if (bowlId == BOWL_ID_ONE_BOWL_USED) { + updateState(PET_CHANNEL_FEEDER_LAST_CHANGE, + new QuantityType(feeding.feedChange.get(0), SIUnits.GRAM)); + } else if (bowlId == BOWL_ID_TWO_BOWLS_USED) { + updateState(PET_CHANNEL_FEEDER_LAST_CHANGE_LEFT, + new QuantityType(feeding.feedChange.get(0), SIUnits.GRAM)); + if (numBowls > 1) { + updateState(PET_CHANNEL_FEEDER_LAST_CHANGE_RIGHT, + new QuantityType(feeding.feedChange.get(1), SIUnits.GRAM)); + } + } + } + updateState(PET_CHANNEL_FEEDER_LASTFEEDING, new DateTimeType(feeding.feedChangeAt)); + } + } + } else { + logger.debug("Trying to update unknown pet: {}", thing.getUID().getId()); + } + } + } + + /** + * Tries to lookup image in cache. If not found, it tries to download the image from its URL. + * + * @param url the url of the pet photo + * @return the pet image as {@link RawType} or UNDEF + */ + private State getPetPhotoFromCache(@Nullable String url) { + if (url == null) { + return UnDefType.UNDEF; + } + if (IMAGE_CACHE.containsKey(url)) { + try { + byte[] bytes = IMAGE_CACHE.get(url); + String contentType = HttpUtil.guessContentTypeFromData(bytes); + return new RawType(bytes, + contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType); + } catch (IOException e) { + logger.trace("Failed to download the content of URL '{}'", url, e); + } + } else { + // photo is not yet in cache, download and add + RawType image = HttpUtil.downloadImage(url); + if (image != null) { + IMAGE_CACHE.put(url, image.getBytes()); + return image; + } + } + return UnDefType.UNDEF; + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonLocalDateTypeAdapter.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonLocalDateTypeAdapter.java new file mode 100644 index 00000000000..c5fecd86eb1 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonLocalDateTypeAdapter.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.utils; + +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * GSON serialiser/deserialiser for converting {@link LocalDate} objects. + * + * @author Rene Scherer - Initial Contribution + */ +@NonNullByDefault +public class GsonLocalDateTypeAdapter implements JsonSerializer, JsonDeserializer { + + private static final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + private static final DateTimeFormatter ZONED_DATETIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + /** + * Gson invokes this call-back method during serialization when it encounters a field of the + * specified type. + *

+ * + * In the implementation of this call-back method, you should consider invoking + * {@link JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any + * non-trivial field of the {@code src} object. However, you should never invoke it on the + * {@code src} object itself since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param src the object that needs to be converted to Json. + * @param typeOfSrc the actual type (fully genericized version) of the source object. + * @return a JsonElement corresponding to the specified object. + */ + @Override + public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(LOCAL_DATE_FORMATTER.format(src)); + } + + /** + * Gson invokes this call-back method during deserialization when it encounters a field of the + * specified type. + *

+ * + * In the implementation of this call-back method, you should consider invoking + * {@link JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects + * for any non-trivial field of the returned object. However, you should never invoke it on the + * the same type passing {@code json} since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T} + * @throws JsonParseException if json is not in the expected format of {@code typeOfT} + */ + @Override + public @Nullable LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String el = json.getAsString(); + // we allow dates to be represented as dates only or with time and timezone offset + if (el.length() > 10) { + return ZONED_DATETIME_FORMATTER.parse(el, LocalDate::from); + } else { + return LOCAL_DATE_FORMATTER.parse(el, LocalDate::from); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonLocalTimeTypeAdapter.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonLocalTimeTypeAdapter.java new file mode 100644 index 00000000000..edbaf348af3 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonLocalTimeTypeAdapter.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.utils; + +import java.lang.reflect.Type; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * GSON serialiser/deserialiser for converting {@link LocalTime} objects. + * + * @author Rene Scherer - Initial Contribution + */ +@NonNullByDefault +public class GsonLocalTimeTypeAdapter implements JsonSerializer, JsonDeserializer { + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + /** + * Gson invokes this call-back method during serialization when it encounters a field of the + * specified type. + *

+ * + * In the implementation of this call-back method, you should consider invoking + * {@link JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any + * non-trivial field of the {@code src} object. However, you should never invoke it on the + * {@code src} object itself since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param src the object that needs to be converted to Json. + * @param typeOfSrc the actual type (fully genericized version) of the source object. + * @return a JsonElement corresponding to the specified object. + */ + @Override + public JsonElement serialize(LocalTime src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(TIME_FORMATTER.format(src)); + } + + /** + * Gson invokes this call-back method during deserialization when it encounters a field of the + * specified type. + *

+ * + * In the implementation of this call-back method, you should consider invoking + * {@link JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects + * for any non-trivial field of the returned object. However, you should never invoke it on the + * the same type passing {@code json} since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T} + * @throws JsonParseException if json is not in the expected format of {@code typeOfT} + */ + @Override + public @Nullable LocalTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return TIME_FORMATTER.parse(json.getAsString(), LocalTime::from); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonZonedDateTimeTypeAdapter.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonZonedDateTimeTypeAdapter.java new file mode 100644 index 00000000000..a2b64c0bb8d --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/GsonZonedDateTimeTypeAdapter.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.utils; + +import java.lang.reflect.Type; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * GSON serialiser/deserialiser for converting {@link ZonedDateTime} objects. + * + * @author Rene Scherer - Initial Contribution + */ +@NonNullByDefault +public class GsonZonedDateTimeTypeAdapter implements JsonSerializer, JsonDeserializer { + + private static final DateTimeFormatter ZONED_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx"); + + /** + * Gson invokes this call-back method during serialization when it encounters a field of the + * specified type. + *

+ * + * In the implementation of this call-back method, you should consider invoking + * {@link JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any + * non-trivial field of the {@code src} object. However, you should never invoke it on the + * {@code src} object itself since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param src the object that needs to be converted to Json. + * @param typeOfSrc the actual type (fully genericized version) of the source object. + * @return a JsonElement corresponding to the specified object. + */ + @Override + public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(ZONED_FORMATTER.format(src)); + } + + /** + * Gson invokes this call-back method during deserialization when it encounters a field of the + * specified type. + *

+ * + * In the implementation of this call-back method, you should consider invoking + * {@link JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects + * for any non-trivial field of the returned object. However, you should never invoke it on the + * the same type passing {@code json} since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T} + * @throws JsonParseException if json is not in the expected format of {@code typeOfT} + */ + @Override + public @Nullable ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return ZONED_FORMATTER.parse(json.getAsString(), ZonedDateTime::from); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/SurePetcareDeviceCurfewListTypeAdapterFactory.java b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/SurePetcareDeviceCurfewListTypeAdapterFactory.java new file mode 100644 index 00000000000..c2dd47a2e04 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/java/org/openhab/binding/surepetcare/internal/utils/SurePetcareDeviceCurfewListTypeAdapterFactory.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.utils; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfew; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfewList; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.MalformedJsonException; + +/** + * The {@link GsonColonDateTypeAdapter} class is a custom TypeAdapter factory to ensure deserialization always returns a + * list even if the Json document contains only a single curfew object and not an array. + * + * See https://stackoverflow.com/questions/43412261/make-gson-accept-single-objects-where-it-expects-arrays + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public final class SurePetcareDeviceCurfewListTypeAdapterFactory implements TypeAdapterFactory { + + // Gson can instantiate it itself + private SurePetcareDeviceCurfewListTypeAdapterFactory() { + } + + @Override + public @Nullable TypeAdapter create(final @Nullable Gson gson, final @Nullable TypeToken typeToken) { + if (gson == null || typeToken == null) { + return null; + } + final TypeAdapter elementTypeAdapter = gson + .getAdapter(TypeToken.get(SurePetcareDeviceCurfew.class)); + @SuppressWarnings("unchecked") + final TypeAdapter alwaysListTypeAdapter = (TypeAdapter) new SurePetcareDeviceCurfewListTypeAdapter( + elementTypeAdapter).nullSafe(); + return alwaysListTypeAdapter; + } + + private static final class SurePetcareDeviceCurfewListTypeAdapter + extends TypeAdapter> { + + private final TypeAdapter elementTypeAdapter; + + private SurePetcareDeviceCurfewListTypeAdapter(final TypeAdapter elementTypeAdapter) { + this.elementTypeAdapter = elementTypeAdapter; + } + + @Override + public void write(final @Nullable JsonWriter out, @Nullable final List list) + throws IOException { + if (out != null && list != null) { + out.beginArray(); + for (SurePetcareDeviceCurfew curfew : list) { + elementTypeAdapter.write(out, curfew); + } + out.endArray(); + } + } + + @Override + public @Nullable List read(final @Nullable JsonReader in) throws IOException { + // This is where we detect the list "type" + final SurePetcareDeviceCurfewList list = new SurePetcareDeviceCurfewList(); + if (in != null) { + final JsonToken token = in.peek(); + switch (token) { + case BEGIN_ARRAY: + // If it's a regular list, just consume [, , and ] + in.beginArray(); + while (in.hasNext()) { + SurePetcareDeviceCurfew cf = elementTypeAdapter.read(in); + if (cf != null) { + list.add(cf); + } + } + in.endArray(); + break; + case BEGIN_OBJECT: + SurePetcareDeviceCurfew cf = elementTypeAdapter.read(in); + if (cf != null) { + list.add(cf); + } + break; + case NULL: + throw new AssertionError( + "Must never happen: check if the type adapter configured with .nullSafe()"); + case STRING: + case NUMBER: + case BOOLEAN: + case NAME: + case END_ARRAY: + case END_OBJECT: + case END_DOCUMENT: + throw new MalformedJsonException("Unexpected token: " + token); + default: + throw new AssertionError("Must never happen: " + token); + } + } + return list; + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..ad295950edf --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Sure Petcare Binding + This binding interacts with the Sure Petcare Connect range of cat flaps and feeders + Rene Scherer, Holger Eisold + + diff --git a/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/i18n/surepetcare.properties b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/i18n/surepetcare.properties new file mode 100644 index 00000000000..c6548f4d9bb --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/i18n/surepetcare.properties @@ -0,0 +1,266 @@ +binding.surepetcare.name = Sure Petcare Binding +binding.surepetcare.description = This binding interacts with the Sure Petcare Connect range of cat/pet flaps and feeders + +# bridge-types +thing-type.surepetcare.bridge.label = Sure Petcare API Bridge +thing-type.surepetcare.bridge.description = The Sure Petcare Cloud API Bridge. + +# bridge types config +thing-type.config.surepetcare.bridge.username.label = Sure Petcare Username (Email) +thing-type.config.surepetcare.bridge.username.description = The username to access the Sure Petcare API. + +thing-type.config.surepetcare.bridge.password.label = Sure Petcare Password +thing-type.config.surepetcare.bridge.password.description = The password to access the Sure Petcare API. + +thing-type.config.surepetcare.bridge.refreshIntervalStatus.label = Refresh Interval Pet Status +thing-type.config.surepetcare.bridge.refreshIntervalStatus.description = Query interval (in secs) for pet status. (min. 300 / default 300). + +thing-type.config.surepetcare.bridge.refreshIntervalTopology.label = Refresh Interval Topology +thing-type.config.surepetcare.bridge.refreshIntervalTopology.description = Query interval (in secs) for device topology. (min 300 / default 36000). + +# thing-types +thing-type.surepetcare.household.label = Sure Petcare Household +thing-type.surepetcare.household.description = A Sure Petcare household. + +thing-type.surepetcare.hubDevice.label = Sure Petcare Hub Device +thing-type.surepetcare.hubDevice.description = A Sure Petcare hub device (connects cat/pet flaps, feeders etc. to a network). + +thing-type.surepetcare.flapDevice.label = Sure Petcare Flap Device +thing-type.surepetcare.flapDevice.description = A Sure Petcare cat or pet flap device. + +thing-type.surepetcare.feederDevice.label = Sure Petcare Pet Feeder +thing-type.surepetcare.feederDevice.description = A Sure Petcare pet feeder device. + +thing-type.surepetcare.pet.label = Sure Petcare Pet +thing-type.surepetcare.pet.description = A Sure Petcare pet. + +# channel-types +channel-type.surepetcare.refreshType.label = Refresh +channel-type.surepetcare.refreshType.description = Triggers a cache refresh of everything (devices, pets etc.). + +channel-type.surepetcare.idType.label = ID +channel-type.surepetcare.idType.description = A unique id of the thing. + +channel-type.surepetcare.nameType.label = Name +channel-type.surepetcare.nameType.description = The name of the thing. + +channel-type.surepetcare.commentType.label = Comments +channel-type.surepetcare.commentType.description = Comments about the thing. + +channel-type.surepetcare.timezoneIdType.label = Timezone ID +channel-type.surepetcare.timezoneIdType.description = The identifier of the timezone. + +channel-type.surepetcare.productType.label = Product Type +channel-type.surepetcare.productType.description = The type of product this device represents. (0=Unknown, 1=Hub, 3=Pet Flap, 4=Feeder, 6=Cat Flap) +channel-type.surepetcare.productType.state.option.0 = Unknown +channel-type.surepetcare.productType.state.option.1 = Hub +channel-type.surepetcare.productType.state.option.3 = Pet Flap +channel-type.surepetcare.productType.state.option.4 = Pet Feeder +channel-type.surepetcare.productType.state.option.6 = Cat Flap + +channel-type.surepetcare.shareCodeType.label = Share Code +channel-type.surepetcare.shareCodeType.description = A unique code provided by Sure Petcare to access photos of pets. + +channel-type.surepetcare.genderType.label = Gender +channel-type.surepetcare.genderType.description = The gender of the pet. +channel-type.surepetcare.genderType.state.option.0 = Female +channel-type.surepetcare.genderType.state.option.1 = Male + +channel-type.surepetcare.breedType.label = Breed +channel-type.surepetcare.breedType.description = The breed of the pet. +channel-type.surepetcare.breedType.state.option.333 = Abyssinian +channel-type.surepetcare.breedType.state.option.334 = American Bobtail +channel-type.surepetcare.breedType.state.option.335 = American Curl +channel-type.surepetcare.breedType.state.option.336 = American Shorthair +channel-type.surepetcare.breedType.state.option.337 = American Wirehair +channel-type.surepetcare.breedType.state.option.338 = Balinese +channel-type.surepetcare.breedType.state.option.339 = Bengal +channel-type.surepetcare.breedType.state.option.340 = Birman +channel-type.surepetcare.breedType.state.option.341 = Bombay +channel-type.surepetcare.breedType.state.option.342 = British Shorthair +channel-type.surepetcare.breedType.state.option.343 = Burmese +channel-type.surepetcare.breedType.state.option.344 = Burmilla +channel-type.surepetcare.breedType.state.option.345 = Chartreux +channel-type.surepetcare.breedType.state.option.346 = Chinese Li Hua +channel-type.surepetcare.breedType.state.option.347 = Colorpoint Shorthair +channel-type.surepetcare.breedType.state.option.348 = Cornish Rex +channel-type.surepetcare.breedType.state.option.349 = Cymric +channel-type.surepetcare.breedType.state.option.350 = Devon Rex +channel-type.surepetcare.breedType.state.option.351 = Egyptian Mau +channel-type.surepetcare.breedType.state.option.352 = European Burmese +channel-type.surepetcare.breedType.state.option.353 = Exotic +channel-type.surepetcare.breedType.state.option.354 = Havana Brown +channel-type.surepetcare.breedType.state.option.355 = Himalayan +channel-type.surepetcare.breedType.state.option.356 = Japanese Bobtail +channel-type.surepetcare.breedType.state.option.357 = Javanese +channel-type.surepetcare.breedType.state.option.358 = Korat +channel-type.surepetcare.breedType.state.option.359 = LaPerm +channel-type.surepetcare.breedType.state.option.360 = Maine Coon +channel-type.surepetcare.breedType.state.option.361 = Manx +channel-type.surepetcare.breedType.state.option.362 = Nebelung +channel-type.surepetcare.breedType.state.option.363 = Norwegian Forest +channel-type.surepetcare.breedType.state.option.364 = Ocicat +channel-type.surepetcare.breedType.state.option.365 = Oriental +channel-type.surepetcare.breedType.state.option.366 = Persian +channel-type.surepetcare.breedType.state.option.367 = Ragamuffin +channel-type.surepetcare.breedType.state.option.368 = Ragdoll Cats +channel-type.surepetcare.breedType.state.option.369 = Russisch Blue +channel-type.surepetcare.breedType.state.option.370 = Savannah +channel-type.surepetcare.breedType.state.option.371 = Scottish Fold +channel-type.surepetcare.breedType.state.option.372 = Selkirk Rex +channel-type.surepetcare.breedType.state.option.373 = Siamese Cat +channel-type.surepetcare.breedType.state.option.374 = Siberian +channel-type.surepetcare.breedType.state.option.375 = Singapura +channel-type.surepetcare.breedType.state.option.376 = Snowshoe +channel-type.surepetcare.breedType.state.option.377 = Somali +channel-type.surepetcare.breedType.state.option.378 = Sphynx +channel-type.surepetcare.breedType.state.option.379 = Tonkinese +channel-type.surepetcare.breedType.state.option.380 = Turkish Angora +channel-type.surepetcare.breedType.state.option.381 = Turkish Van +channel-type.surepetcare.breedType.state.option.382 = Domestic Shorthair +channel-type.surepetcare.breedType.state.option.383 = Domestic Longhair +channel-type.surepetcare.breedType.state.option.384 = Moggy + +channel-type.surepetcare.locationType.label = Location +channel-type.surepetcare.locationType.description = The current location of the pet (inside or outside). +channel-type.surepetcare.locationType.state.option.0 = Unknown +channel-type.surepetcare.locationType.state.option.1 = Inside +channel-type.surepetcare.locationType.state.option.2 = Outside + +channel-type.surepetcare.speciesType.label = Species +channel-type.surepetcare.speciesType.description = The species of the pet. +channel-type.surepetcare.speciesType.state.option.0 = Undefined +channel-type.surepetcare.speciesType.state.option.1 = Cat +channel-type.surepetcare.speciesType.state.option.2 = Dog + +channel-type.surepetcare.photoURLType.label = Photo URL +channel-type.surepetcare.photoURLType.description = The URL of the pet photo. + +channel-type.surepetcare.tagIdentifierType.label = Micro Chip Tag Identifier +channel-type.surepetcare.tagIdentifierType.description = The unique identifier of the pet micro chip tag. + +channel-type.surepetcare.locationChangedType.label = Last Location Change Time +channel-type.surepetcare.locationChangedType.description = The time when the pet location has last changed. + +channel-type.surepetcare.locationTimeoffsetType.label = Location Change Time Offset +channel-type.surepetcare.locationTimeoffsetType.description = Time-Command to set the pet location with a time offset. (10, 30 or 60 minutes ago). +channel-type.surepetcare.locationTimeoffsetType.state.option.10 = 10 mins ago +channel-type.surepetcare.locationTimeoffsetType.state.option.30 = 30 mins ago +channel-type.surepetcare.locationTimeoffsetType.state.option.60 = 60 mins ago + +channel-type.surepetcare.locationChangedThroughType.label = Last Location Changed By +channel-type.surepetcare.locationChangedThroughType.description = Indicates by which user or device the last location change was made. + +channel-type.surepetcare.ledModeType.label = LED Mode +channel-type.surepetcare.ledModeType.description = The current LED mode of the device. +channel-type.surepetcare.ledModeType.state.option.0 = Off +channel-type.surepetcare.ledModeType.state.option.1 = Bright +channel-type.surepetcare.ledModeType.state.option.4 = Dimmed + +channel-type.surepetcare.pairingModeType.label = Pairing Mode +channel-type.surepetcare.pairingModeType.description = The current pairing mode of the device. +channel-type.surepetcare.pairingModeType.state.option.0 = Normal Mode +channel-type.surepetcare.pairingModeType.state.option.1 = Pairing Mode + +channel-type.surepetcare.onlineType.label = Online State +channel-type.surepetcare.onlineType.description = Indicator if device is online or offline. + +channel-type.surepetcare.curfewEnabledType.label = Curfew Enabled +channel-type.surepetcare.curfewEnabledType.description = Indicator if the curfew is enabled. + +channel-type.surepetcare.curfewLockTimeType.label = Curfew Lock Time +channel-type.surepetcare.curfewLockTimeType.description = The time when the curfew starts. + +channel-type.surepetcare.curfewUnlockTimeType.label = Curfew Unlock Time +channel-type.surepetcare.curfewUnlockTimeType.description = The time when the curfew finishes. + +channel-type.surepetcare.dateOfBirthType.label = Pet Birthday +channel-type.surepetcare.dateOfBirthType.description = The pet birthday. + +channel-type.surepetcare.weightType.label = Pet Weight +channel-type.surepetcare.weightType.description = The pet weight + +channel-type.surepetcare.feederDeviceType.label = Pet Feeding Device Name +channel-type.surepetcare.feederDeviceType.description = The pet feeding device name. + +channel-type.surepetcare.feederLastFeedingType.label = Pet Last Feeding +channel-type.surepetcare.feederLastFeedingType.description = The last pet feeding. + +channel-type.surepetcare.feederLastChangeType.label = Pet Feeding Last Change +channel-type.surepetcare.feederLastChangeType.description = The pet feeding last change. + +channel-type.surepetcare.feederLastChangeLeftType.label = Pet Feeding Last Change Left +channel-type.surepetcare.feederLastChangeLeftType.description = The pet feeding last change left. + +channel-type.surepetcare.feederLastChangeRightType.label = Pet Feeding Last Change Right +channel-type.surepetcare.feederLastChangeRightType.description = The pet feeding last change right. + +channel-type.surepetcare.lockingModeType.label = Locking Mode +channel-type.surepetcare.lockingModeType.description = The id of the locking mode the flap is currently set to (e.g. in/out, in only, out only, locked) +channel-type.surepetcare.lockingModeType.state.option.0 = Unlocked +channel-type.surepetcare.lockingModeType.state.option.1 = Locked In-only +channel-type.surepetcare.lockingModeType.state.option.2 = Locked Out-only +channel-type.surepetcare.lockingModeType.state.option.3 = Locked +channel-type.surepetcare.lockingModeType.state.option.4 = Curfew + +channel-type.surepetcare.batteryVoltageType.label = Battery Voltage +channel-type.surepetcare.batteryVoltageType.description = The current battery voltage. + +channel-type.surepetcare.rssiDeviceType.label = Signal Strength Device (RSSI) +channel-type.surepetcare.rssiDeviceType.description = The received device signal strength (RSSI). + +channel-type.surepetcare.rssiHubType.label = Signal Strength Hub (RSSI) +channel-type.surepetcare.rssiHubType.description = The received hub signal strength (RSSI). + +channel-type.surepetcare.bowlsFoodType.label = Bowls Food Type +channel-type.surepetcare.bowlsFoodType.description = The food type of the bowl if the big bowl is used. +channel-type.surepetcare.bowlsFoodType.state.option.1 = Wet Food +channel-type.surepetcare.bowlsFoodType.state.option.2 = Dry Food +channel-type.surepetcare.bowlsFoodType.state.option.3 = Wet & Dry Food + +channel-type.surepetcare.bowlsTargetType.label = Bowls Food Type (Big Bowl) +channel-type.surepetcare.bowlsTargetType.description = The weight target of the bowl if the big bowl is used. + +channel-type.surepetcare.bowlsFoodLeftType.label = Bowls Food Type Half Bowl (Left) +channel-type.surepetcare.bowlsFoodLeftType.description = The food type of the left bowl if the half bowls are used. +channel-type.surepetcare.bowlsFoodLeftType.state.option.1 = Wet Food +channel-type.surepetcare.bowlsFoodLeftType.state.option.2 = Dry Food +channel-type.surepetcare.bowlsFoodLeftType.state.option.3 = Wet & Dry Food + +channel-type.surepetcare.bowlsTargetLeftType.label = Bowls Target Half Bowl (Left) +channel-type.surepetcare.bowlsTargetLeftType.description = The weight target of the left bowl if the half bowls are used. + +channel-type.surepetcare.bowlsFoodRightType.label = Bowls Food Type Half Bowl (Right) +channel-type.surepetcare.bowlsFoodRightType.description = The food type of the right bowl if the half bowls are used. +channel-type.surepetcare.bowlsFoodRightType.state.option.1 = Wet Food +channel-type.surepetcare.bowlsFoodRightType.state.option.2 = Dry Food +channel-type.surepetcare.bowlsFoodRightType.state.option.3 = Wet & Dry Food + +channel-type.surepetcare.bowlsTargetRightType.label = Bowls Target Half Bowl (Right) +channel-type.surepetcare.bowlsTargetRightType.description = The weight target of the right bowl if the half bowls are used. + +channel-type.surepetcare.bowlsType.label = Bowls Type +channel-type.surepetcare.bowlsType.description = The type of used bowls. +channel-type.surepetcare.bowlsType.state.option.0 = Undefined +channel-type.surepetcare.bowlsType.state.option.1 = Big Bowl +channel-type.surepetcare.bowlsType.state.option.4 = Half Bowls + +channel-type.surepetcare.bowlsCloseDelayType.label = Feeder Lid Close Delay +channel-type.surepetcare.bowlsCloseDelayType.description = The feeder lid close delay +channel-type.surepetcare.bowlsCloseDelayType.state.option.0 = Fast +channel-type.surepetcare.bowlsCloseDelayType.state.option.4 = Normal +channel-type.surepetcare.bowlsCloseDelayType.state.option.20 = Slow + +channel-type.surepetcare.bowlsTrainingModeType.label = Feeder Training Mode +channel-type.surepetcare.bowlsTrainingModeType.description = The feeder training mode. +channel-type.surepetcare.bowlsTrainingModeType.state.option.0 = Off +channel-type.surepetcare.bowlsTrainingModeType.state.option.1 = Full open +channel-type.surepetcare.bowlsTrainingModeType.state.option.2 = Almost full open +channel-type.surepetcare.bowlsTrainingModeType.state.option.3 = Half closed +channel-type.surepetcare.bowlsTrainingModeType.state.option.4 = Almost closed + +# Thing status description +offline.conf-error-invalid-refresh-intervals = Invalid refresh interval. +offline.conf-error-missing-username-or-password = Missing username or password. +offline.conf-error-authentication = Authentication Error. + diff --git a/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/i18n/surepetcare_de.properties b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/i18n/surepetcare_de.properties new file mode 100644 index 00000000000..b78c4fc436e --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/i18n/surepetcare_de.properties @@ -0,0 +1,266 @@ +binding.surepetcare.name = Sure Petcare Binding +binding.surepetcare.description = Dieses Binding stellt eine Verbindung mit der Sure Petcare API her. + +# bridge-types +thing-type.surepetcare.bridge.label = Sure Petcare API Konto +thing-type.surepetcare.bridge.description = Ermöglicht den Zugriff auf die Sure Petcare API. + +# bridge types config +thing-type.config.surepetcare.bridge.username.label = Sure Petcare Benutzername +thing-type.config.surepetcare.bridge.username.description = Sure Petcare Benutzername oder Email-Adresse. + +thing-type.config.surepetcare.bridge.password.label = Sure Petcare Passwort +thing-type.config.surepetcare.bridge.password.description = Sure Petcare Passwort. + +thing-type.config.surepetcare.bridge.refreshIntervalStatus.label = Abfrageintervall Haustier Status +thing-type.config.surepetcare.bridge.refreshIntervalStatus.description = Intervall zur Abfrage des Haustier Status (in Sekunden) (mind. 300 / standard 36000). + +thing-type.config.surepetcare.bridge.refreshIntervalTopology.label = Abfrageintervall Topology +thing-type.config.surepetcare.bridge.refreshIntervalTopology.description = Intervall zur Abfrage der Geräte und Haustier Daten (in Sekunden) (mind. 300 / standard 300). + +# thing-types +thing-type.surepetcare.household.label = Sure Petcare Haushalt +thing-type.surepetcare.household.description = Ein Sure Petcare Haushalt. + +thing-type.surepetcare.hubDevice.label = Sure Petcare Gerät (Hub) +thing-type.surepetcare.hubDevice.description = Ein Sure Petcare Gerät (Hub). + +thing-type.surepetcare.flapDevice.label = Sure Petcare Gerät (Haustier-/Katzenklappe) +thing-type.surepetcare.flapDevice.description = Ein Sure Petcare Gerät (Haustier-/Katzenklappe). + +thing-type.surepetcare.feederDevice.label = Sure Petcare Gerät (Futterautomat) +thing-type.surepetcare.feederDevice.description = Sure Petcare Gerät (Futterautomat). + +thing-type.surepetcare.pet.label = Sure Petcare Haustier +thing-type.surepetcare.pet.description = Ein Sure Petcare Haustier. + +# channel-types +channel-type.surepetcare.refreshType.label = Aktualisieren +channel-type.surepetcare.refreshType.description = Löst die Aktualisierung der Geräte- und Haustierdaten aus. + +channel-type.surepetcare.idType.label = ID +channel-type.surepetcare.idType.description = Zeigt die ID an. + +channel-type.surepetcare.nameType.label = Name +channel-type.surepetcare.nameType.description = Zeigt den Namen an. + +channel-type.surepetcare.commentType.label = Kommentar +channel-type.surepetcare.commentType.description = Zeigt den Kommentar an. + +channel-type.surepetcare.timezoneIdType.label = Zeitzone ID +channel-type.surepetcare.timezoneIdType.description = Zeigt die ID der Zeitzone an. + +channel-type.surepetcare.productType.label = Produkt Typ +channel-type.surepetcare.productType.description = Zeigt den Produkt Namen an. #(0=unbekannt, 1=Hub, 3=Haustierklappe, 4=Futterautomat, 6=Katzenklappe) +channel-type.surepetcare.productType.state.option.0 = unbekannt +channel-type.surepetcare.productType.state.option.1 = Hub +channel-type.surepetcare.productType.state.option.3 = Haustierklappe +channel-type.surepetcare.productType.state.option.4 = Futterautomat +channel-type.surepetcare.productType.state.option.6 = Katzenklappe + +channel-type.surepetcare.shareCodeType.label = Share Code +channel-type.surepetcare.shareCodeType.description = A unique code provided by Sure Petcare to access photos of pets + +channel-type.surepetcare.genderType.label = Geschlecht +channel-type.surepetcare.genderType.description = Zeigt das Geschlecht des Haustieres an. +channel-type.surepetcare.genderType.state.option.0 = Weiblich +channel-type.surepetcare.genderType.state.option.1 = Männlich + +channel-type.surepetcare.breedType.label = Rasse +channel-type.surepetcare.breedType.description = Zeigt die Rasse des Haustieres an. +channel-type.surepetcare.breedType.state.option.333 = Abessinierkatze +channel-type.surepetcare.breedType.state.option.334 = American Bobtail +channel-type.surepetcare.breedType.state.option.335 = American Curl +channel-type.surepetcare.breedType.state.option.336 = American Shorthair +channel-type.surepetcare.breedType.state.option.337 = American Wirehair +channel-type.surepetcare.breedType.state.option.338 = Balinesenkatze +channel-type.surepetcare.breedType.state.option.339 = Bengal +channel-type.surepetcare.breedType.state.option.340 = Birma-Katze +channel-type.surepetcare.breedType.state.option.341 = Bombay-Katze +channel-type.surepetcare.breedType.state.option.342 = British Kurzhaar +channel-type.surepetcare.breedType.state.option.343 = Burma-Katze +channel-type.surepetcare.breedType.state.option.344 = Burmilla +channel-type.surepetcare.breedType.state.option.345 = Chartreux +channel-type.surepetcare.breedType.state.option.346 = Chinese Li Hua +channel-type.surepetcare.breedType.state.option.347 = Colorpoint Shorthair +channel-type.surepetcare.breedType.state.option.348 = Cornish Rex +channel-type.surepetcare.breedType.state.option.349 = Cymric +channel-type.surepetcare.breedType.state.option.350 = Devon Rex +channel-type.surepetcare.breedType.state.option.351 = Ägyptische Mau +channel-type.surepetcare.breedType.state.option.352 = Burma-Katze +channel-type.surepetcare.breedType.state.option.353 = Exotische Kurzhaarkatze +channel-type.surepetcare.breedType.state.option.354 = Havana-Katze +channel-type.surepetcare.breedType.state.option.355 = Colourpoint +channel-type.surepetcare.breedType.state.option.356 = Japanese Bobtail +channel-type.surepetcare.breedType.state.option.357 = Orientalisch Langhaar +channel-type.surepetcare.breedType.state.option.358 = Korat-Katze +channel-type.surepetcare.breedType.state.option.359 = LaPerm +channel-type.surepetcare.breedType.state.option.360 = Maine Coon +channel-type.surepetcare.breedType.state.option.361 = Manx +channel-type.surepetcare.breedType.state.option.362 = Nebelung-Katze +channel-type.surepetcare.breedType.state.option.363 = Norwegische Waldkatze +channel-type.surepetcare.breedType.state.option.364 = Ocicat +channel-type.surepetcare.breedType.state.option.365 = Orientalisch Kurzhaar +channel-type.surepetcare.breedType.state.option.366 = Persisch +channel-type.surepetcare.breedType.state.option.367 = Perserkatze +channel-type.surepetcare.breedType.state.option.368 = Ragdoll-Katze +channel-type.surepetcare.breedType.state.option.369 = Russisch Blau +channel-type.surepetcare.breedType.state.option.370 = Savannah-Katze +channel-type.surepetcare.breedType.state.option.371 = Schottische Faltohrkatze +channel-type.surepetcare.breedType.state.option.372 = Selkirk Rex +channel-type.surepetcare.breedType.state.option.373 = Siamkatze +channel-type.surepetcare.breedType.state.option.374 = Sibirische Katze +channel-type.surepetcare.breedType.state.option.375 = Singapura +channel-type.surepetcare.breedType.state.option.376 = Snowshoe +channel-type.surepetcare.breedType.state.option.377 = Somali +channel-type.surepetcare.breedType.state.option.378 = Sphynx-Katze +channel-type.surepetcare.breedType.state.option.379 = Tonkanese +channel-type.surepetcare.breedType.state.option.380 = Türkisch Angora +channel-type.surepetcare.breedType.state.option.381 = Türkisch Van +channel-type.surepetcare.breedType.state.option.382 = Kurzhaarige Hauskatze +channel-type.surepetcare.breedType.state.option.383 = Langhaarige Hauskatze +channel-type.surepetcare.breedType.state.option.384 = Wildkatze + +channel-type.surepetcare.locationType.label = Standort +channel-type.surepetcare.locationType.description = Zeigt den Standort des Haustieres an. +channel-type.surepetcare.locationType.state.option.0 = Unbekannt +channel-type.surepetcare.locationType.state.option.1 = Im Haus +channel-type.surepetcare.locationType.state.option.2 = Draußen + +channel-type.surepetcare.speciesType.label = Tierart +channel-type.surepetcare.speciesType.description = Zeigt die Tierart des Haustieres an. +channel-type.surepetcare.speciesType.state.option.0 = Undefiniert +channel-type.surepetcare.speciesType.state.option.1 = Katze +channel-type.surepetcare.speciesType.state.option.2 = Hund + +channel-type.surepetcare.photoURLType.label = Foto URL +channel-type.surepetcare.photoURLType.description = Zeigt die URL des Haustier Fotos an. + +channel-type.surepetcare.tagIdentifierType.label = Micro Chip Tag +channel-type.surepetcare.tagIdentifierType.description = Zeigt den Tag des Haustier Microchips an. + +channel-type.surepetcare.locationChangedType.label = Standort Zeit +channel-type.surepetcare.locationChangedType.description = Zeigt die Zeit des letzten Standortwechsels an. + +channel-type.surepetcare.locationTimeoffsetType.label = Standort Wechsel Zeitversatz +channel-type.surepetcare.locationTimeoffsetType.description = Änderung des Haustier Standortes mit einem Zeitversatz. (vor 10, 30 oder 60 Minuten). +channel-type.surepetcare.locationTimeoffsetType.state.option.10 = vor 10 min +channel-type.surepetcare.locationTimeoffsetType.state.option.30 = vor 30 min +channel-type.surepetcare.locationTimeoffsetType.state.option.60 = vor 60 min + +channel-type.surepetcare.locationChangedThroughType.label = Standort geändert durch +channel-type.surepetcare.locationChangedThroughType.description = Zeigt an durch welchen Benutzer oder welches Gerät die letzte Standort-Änderung durchgeführt wurde. + +channel-type.surepetcare.ledModeType.label = LED Modus +channel-type.surepetcare.ledModeType.description = Zeigt den LED Modus des Hubs an. +channel-type.surepetcare.ledModeType.state.option.0 = Aus +channel-type.surepetcare.ledModeType.state.option.1 = Hell +channel-type.surepetcare.ledModeType.state.option.4 = Dunkel + +channel-type.surepetcare.pairingModeType.label = Paarungs Modus +channel-type.surepetcare.pairingModeType.description = Zeigt den Paarungs Modus des Hubs an. +channel-type.surepetcare.pairingModeType.state.option.0 = Normaler Modus +channel-type.surepetcare.pairingModeType.state.option.1 = Paarungsmodus + +channel-type.surepetcare.onlineType.label = Online Status +channel-type.surepetcare.onlineType.description = Zeigt den Online Status des Gerätes an. + +channel-type.surepetcare.curfewEnabledType.label = Ausgangssperre aktiv +channel-type.surepetcare.curfewEnabledType.description = Zeigt an ob die Ausgangssperre aktiviert ist. + +channel-type.surepetcare.curfewLockTimeType.label = Ausgangssperre Sperrzeit +channel-type.surepetcare.curfewLockTimeType.description = Zeigt die Sperrzeit der Ausgangssperre an. + +channel-type.surepetcare.curfewUnlockTimeType.label = Ausgangssperre Entsperrzeit +channel-type.surepetcare.curfewUnlockTimeType.description = Zeigt die Entsperrzeit der Ausgangssperre an. + +channel-type.surepetcare.dateOfBirthType.label = Geburtstag +channel-type.surepetcare.dateOfBirthType.description = Zeigt den Geburtstag des Haus an. + +channel-type.surepetcare.weightType.label = Gewicht +channel-type.surepetcare.weightType.description = Zeigt das Gewicht des Haustieres an. + +channel-type.surepetcare.feederDeviceType.label = Futterautomat Name +channel-type.surepetcare.feederDeviceType.description = Der Name des Futterautomats. + +channel-type.surepetcare.feederLastFeedingType.label = Letzte Futteraufnahme +channel-type.surepetcare.feederLastFeedingType.description = Die letzte Futteraufnahme. + +channel-type.surepetcare.feederLastChangeType.label = Letzte Futteraufnahme Änderung +channel-type.surepetcare.feederLastChangeType.description = Die letzte Futteraufnahme Änderung. + +channel-type.surepetcare.feederLastChangeLeftType.label = Letzte Futteraufnahme Änderung (Links) +channel-type.surepetcare.feederLastChangeLeftType.description = Die letzte Futteraufnahme Änderung (links). + +channel-type.surepetcare.feederLastChangeRightType.label = Letzte Futteraufnahme Änderung (Rechts) +channel-type.surepetcare.feederLastChangeRightType.description = Die letzte Futteraufnahme Änderung (rechts). + +channel-type.surepetcare.lockingModeType.label = Sperrmodus +channel-type.surepetcare.lockingModeType.description = Zeigt des Sperrmodus des Gerätes an. +channel-type.surepetcare.lockingModeType.state.option.0 = Entriegelt +channel-type.surepetcare.lockingModeType.state.option.1 = Verriegelt (nur hinein) +channel-type.surepetcare.lockingModeType.state.option.2 = Verriegelt (nur hinaus) +channel-type.surepetcare.lockingModeType.state.option.3 = Verriegelt +channel-type.surepetcare.lockingModeType.state.option.4 = Ausgangssperre + +channel-type.surepetcare.batteryVoltageType.label = Batterie Spannung +channel-type.surepetcare.batteryVoltageType.description = Zeigt die Spannung der Batterie an. (in Volt) + +channel-type.surepetcare.rssiDeviceType.label = Signalstärke (Gerät) +channel-type.surepetcare.rssiDeviceType.description = Zeigt die Signalstärke des Gerätes an. + +channel-type.surepetcare.rssiHubType.label = Signalstärke (Hub) +channel-type.surepetcare.rssiHubType.description = Zeigt die Signalstärke des Hubs an. + +channel-type.surepetcare.bowlsFoodType.label = Napf Futter Typ (Großer Napf) +channel-type.surepetcare.bowlsFoodType.description = Zeigt den Futter Typ des großen Napfes an. +channel-type.surepetcare.bowlsFoodType.state.option.1 = Nassfutter +channel-type.surepetcare.bowlsFoodType.state.option.2 = Trockenfutter +channel-type.surepetcare.bowlsFoodType.state.option.3 = Nass- u. Trockenfutter + +channel-type.surepetcare.bowlsTargetType.label = Napf Gewicht (Großer Napf) +channel-type.surepetcare.bowlsTargetType.description = Zeigt das Zielgewicht des Futters vom großen Napf an. + +channel-type.surepetcare.bowlsFoodLeftType.label = Napf Futter Typ Links (Halbe Näpfe) +channel-type.surepetcare.bowlsFoodLeftType.description = Zeigt den Futter Typ des linken kleinen Napfes an. +channel-type.surepetcare.bowlsFoodLeftType.state.option.1 = Nassfutter +channel-type.surepetcare.bowlsFoodLeftType.state.option.2 = Trockenfutter +channel-type.surepetcare.bowlsFoodLeftType.state.option.3 = Nass- u. Trockenfutter + +channel-type.surepetcare.bowlsTargetLeftType.label = Napf Gewicht Links (Halbe Näpfe) +channel-type.surepetcare.bowlsTargetLeftType.description = Zeigt das Zielgewicht des Futters vom linken kleinen Napf an. + +channel-type.surepetcare.bowlsFoodRightType.label = Napf Futter Typ Rechts (Halbe Näpfe) +channel-type.surepetcare.bowlsFoodRightType.description = Zeigt den Futter Typ des rechten kleinen Napfes an. +channel-type.surepetcare.bowlsFoodRightType.state.option.1 = Nassfutter +channel-type.surepetcare.bowlsFoodRightType.state.option.2 = Trockenfutter +channel-type.surepetcare.bowlsFoodRightType.state.option.3 = Nass- u. Trockenfutter + +channel-type.surepetcare.bowlsTargetRightType.label = Napf Gewicht Rechts (Halbe Näpfe) +channel-type.surepetcare.bowlsTargetRightType.description = Zeigt das Zielgewicht des Futters vom rechten großen Napf an. + +channel-type.surepetcare.bowlsType.label = Napf Typ +channel-type.surepetcare.bowlsType.description = Zeigt den Typ des Napfes an. +channel-type.surepetcare.bowlsType.state.option.0 = Undefiniert +channel-type.surepetcare.bowlsType.state.option.1 = Großer Napf +channel-type.surepetcare.bowlsType.state.option.4 = Halbe Näpfe + +channel-type.surepetcare.bowlsCloseDelayType.label = Deckel Schließverzögerung +channel-type.surepetcare.bowlsCloseDelayType.description = Zeigt die Schließverzögerung des Deckels an. +channel-type.surepetcare.bowlsCloseDelayType.state.option.0 = Schnell +channel-type.surepetcare.bowlsCloseDelayType.state.option.4 = Normal +channel-type.surepetcare.bowlsCloseDelayType.state.option.20 = Langsam + +channel-type.surepetcare.bowlsTrainingModeType.label = Futterautomat Training Modus +channel-type.surepetcare.bowlsTrainingModeType.description = Zeigt den Trainings Modus des Futterautomats an. +channel-type.surepetcare.bowlsTrainingModeType.state.option.0 = Aus +channel-type.surepetcare.bowlsTrainingModeType.state.option.1 = Ganz geöffnet +channel-type.surepetcare.bowlsTrainingModeType.state.option.2 = Fast ganz geöffnet +channel-type.surepetcare.bowlsTrainingModeType.state.option.3 = Halb geschlossen +channel-type.surepetcare.bowlsTrainingModeType.state.option.4 = Fast geschlossen + +# Thing status description +offline.conf-error-invalid-refresh-intervals = Ungültiger Aktualisierungs-Intervall. +offline.conf-error-missing-username-or-password = Benutzername oder Passowrt fehlt. +offline.conf-error-authentication = Authentifizierungs-Fehler. + diff --git a/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/thing/things.xml b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/thing/things.xml new file mode 100644 index 00000000000..cf7e723b7d2 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/thing/things.xml @@ -0,0 +1,665 @@ + + + + + + + + The Sure Petcare Cloud API Bridge + + + + + + + + + The username to access the Sure Petcare API + + + + The password to access the Sure Petcare API + password + + + + Query interval (in secs) for device topology + 36000 + + + + Query interval (in secs) for pet status + 300 + + + + + + + Switch + + Triggers a cache refresh of everything (devices, pets etc.). + + + + + + + + + + + + + A Sure Petcare household + + + + + + + + + + + + + + id + + + + + + + + + + A Sure Petcare hub device (connects cat flaps, feeders etc. to a network) + + + + + + + + + + + + + + + + + + + + id + + + + + + + + + + A Sure Petcare cat or pet flap device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + + + + + + + A Sure Petcare pet feeder device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + + + + + + + A Sure Petcare pet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + + + Number + + A unique id of the thing + + + + + String + + The name of the thing + + + + + String + + Comments about the thing + + + + + Number + + The identifier of the timezone + + + + + String + + The type of product this device represents + + + + + + + + + + + + + String + + A unique code provided by Sure Petcare to access photos of pets + + + + + String + + The gender of the pet + + + + + + + + + + String + + The breed of the pet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + The species of the pet + + + + + + + + + + + Image + + The image of the pet + + + + + String + + The unique identifier of the pet micro chip tag + + + + + String + + The current location of the pet + + + + + + + + + + + DateTime + + The time when the pet location has last changed + + + + + String + + The location update of the pet with time offset (10, 30, 60 minutes ago.) + + + + + + + + + + + String + + Shows the device name or username of the last location change. + + + + + String + + The current LED mode of the device + + + + + + + + + + + String + + The current pairing mode of the device + + + + + + + + + + Switch + + Indicator if device is online or offline + + + + + Switch + + Indicator if the curfew is enabled + + + + + String + + The time when the curfew starts + + + + + String + + The time when the curfew finishes + + + + + DateTime + + The pet's date of birth. + Time + + + + + Number:Mass + + The pet weight + + + + + String + + The pet feeding device name + + + + + DateTime + + The last pet feeding + + + + + Number:Mass + + The pet feeding last change + + + + + Number:Mass + + The pet feeding last change left + + + + + Number:Mass + + The pet feeding last change right + + + + + String + + The id of the locking mode the flap is currently set to + + + + + + + + + + + + + Number:ElectricPotential + + The current battery voltage + + + + + Number + + The received device signal strength (RSSI). + + + + + Number + + The received hub signal strength (RSSI). + + + + + String + + The food type of the bowl if the big bowl is used. + + + + + + + + + + + Number:Mass + + The weight target of the bowl if the big bowl is used. + + + + + String + + The food type of the left bowl if the small bowls are used. + + + + + + + + + + + Number:Mass + + The weight target of the left bowl if the small bowls are used. + + + + + String + + The food type of the right bowl if the small bowls are used. + + + + + + + + + + + Number:Mass + + The weight target of the right bowl if the small bowls are used. + + + + + String + + The type of used bowls. + + + + + + + + + + + String + + The feeder lid close delay + + + + + + + + + + + String + + The feeder training mode. + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareBaseObjectTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareBaseObjectTest.java new file mode 100644 index 00000000000..a1e4748b1e2 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareBaseObjectTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareBaseObject; + +/** + * The {@link SurePetcareBaseObjectTest} class implements unit test case for {@link SurePetcareBaseObject} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareBaseObjectTest { + + @Test + public void testNotNullFromJson() { + String testResponse = "{\"id\":2491083182,\"version\":\"MA==\",\"created_at\":\"2019-09-18T16:09:30+00:00\",\"updated_at\":\"2019-09-18T16:09:30+00:00\"}"; + SurePetcareBaseObject response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareBaseObject.class); + if (response != null) { + assertEquals(Long.valueOf(2491083182L), response.id); + assertEquals("MA==", response.version); + assertNotNull(response.createdAt); + assertNotNull(response.updatedAt); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testNullAttributesFromJson() { + String testResponse = "{\"id\":33421}"; + SurePetcareBaseObject response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareBaseObject.class); + + if (response != null) { + assertEquals(Long.valueOf(33421), response.id); + assertEquals("", response.version); + assertNotNull(response.createdAt); + assertNotNull(response.updatedAt); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceControlTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceControlTest.java new file mode 100644 index 00000000000..3da8d77bd91 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceControlTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.text.ParseException; +import java.time.LocalTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceControl; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfew; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfewList; + +/** + * The {@link SurePetcareDeviceControlTest} class implements unit test case for {@link SurePetcareDeviceControl} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareDeviceControlTest { + + @Test + public void testJsonDeserializeCurfewArray() throws ParseException { + String testResponse = "{\"curfew\":[{\"enabled\":true,\"lock_time\":\"19:30\",\"unlock_time\":\"07:00\"}],\"locking\":0,\"fast_polling\":false}"; + SurePetcareDeviceControl response = SurePetcareConstants.GSON.fromJson(testResponse, + SurePetcareDeviceControl.class); + if (response != null) { + assertEquals(1, response.curfewList.size()); + assertEquals(Integer.valueOf(0), response.lockingModeId); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonDeserializeSingleCurfew() throws ParseException { + String testResponse = "{\"curfew\":{\"enabled\":true,\"lock_time\":\"19:00\",\"unlock_time\":\"08:00\"},\"fast_polling\":true}"; + + SurePetcareDeviceControl response = SurePetcareConstants.GSON.fromJson(testResponse, + SurePetcareDeviceControl.class); + if (response != null) { + assertEquals(1, response.curfewList.size()); + assertEquals(true, response.fastPolling); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonSerializeLockingMode() throws ParseException { + SurePetcareDeviceControl control = new SurePetcareDeviceControl(); + control.lockingModeId = Integer.valueOf(4); + + String json = SurePetcareConstants.GSON.toJson(control); + assertEquals("{\"locking\":4}", json); + } + + @Test + public void testJsonSerializeCurfew() throws ParseException { + SurePetcareDeviceControl control = new SurePetcareDeviceControl(); + SurePetcareDeviceCurfewList curfews = new SurePetcareDeviceCurfewList(); + curfews.add(new SurePetcareDeviceCurfew(true, LocalTime.of(19, 30, 00), LocalTime.of(07, 00, 00))); + control.curfewList = curfews; + + String json = SurePetcareConstants.GSON.toJson(control); + assertEquals("{\"curfew\":[{\"enabled\":true,\"lock_time\":\"19:30\",\"unlock_time\":\"07:00\"}]}", json); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceCurfewListTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceCurfewListTest.java new file mode 100644 index 00000000000..e4af8e4ba7a --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceCurfewListTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfew; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDeviceCurfewList; + +/** + * The {@link SurePetcareDeviceCurfewListTest} class implements unit test case for {@link SurePetcareDeviceCurfewList} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareDeviceCurfewListTest { + + @Test + public void testGet() { + SurePetcareDeviceCurfewList orig = new SurePetcareDeviceCurfewList(); + orig.add(new SurePetcareDeviceCurfew()); + orig.add(new SurePetcareDeviceCurfew(true, LocalTime.of(12, 00, 00), LocalTime.of(13, 00, 00))); + + assertEquals(orig.size(), 2); + assertFalse(orig.get(0).enabled); + assertTrue(orig.get(1).enabled); + assertEquals(orig.size(), 2); + + assertFalse(orig.get(2).enabled); + assertEquals(orig.size(), 3); + + assertFalse(orig.get(3).enabled); + assertEquals(orig.size(), 4); + + assertFalse(orig.get(9).enabled); + assertEquals(orig.size(), 10); + } + + @Test + public void testOrder() { + SurePetcareDeviceCurfewList orig = new SurePetcareDeviceCurfewList(); + orig.add(new SurePetcareDeviceCurfew()); + orig.add(new SurePetcareDeviceCurfew(true, LocalTime.of(12, 00, 00), LocalTime.of(13, 00, 00))); + orig.add(new SurePetcareDeviceCurfew(true, LocalTime.of(19, 00, 00), LocalTime.of(20, 00, 00))); + + assertEquals(orig.size(), 3); + assertFalse(orig.get(0).enabled); + assertTrue(orig.get(1).enabled); + assertTrue(orig.get(2).enabled); + + SurePetcareDeviceCurfewList ordered = orig.order(); + assertEquals(ordered.size(), 4); + + assertTrue(ordered.get(0).enabled); + assertEquals(ordered.get(0).lockTime, LocalTime.of(12, 00, 00)); + assertTrue(ordered.get(1).enabled); + assertFalse(ordered.get(2).enabled); + assertFalse(ordered.get(3).enabled); + } + + @Test + public void testCompact() { + SurePetcareDeviceCurfewList orig = new SurePetcareDeviceCurfewList(); + orig.add(new SurePetcareDeviceCurfew()); + orig.add(new SurePetcareDeviceCurfew(true, LocalTime.of(12, 00, 00), LocalTime.of(13, 00, 00))); + orig.add(new SurePetcareDeviceCurfew(true, LocalTime.of(19, 00, 00), LocalTime.of(20, 00, 00))); + orig.add(new SurePetcareDeviceCurfew()); + + assertEquals(orig.size(), 4); + assertFalse(orig.get(0).enabled); + assertTrue(orig.get(1).enabled); + assertTrue(orig.get(2).enabled); + assertFalse(orig.get(3).enabled); + + SurePetcareDeviceCurfewList compact = orig.compact(); + assertEquals(compact.size(), 2); + + assertTrue(compact.get(0).enabled); + assertEquals(compact.get(0).lockTime, LocalTime.of(12, 00, 00)); + assertTrue(compact.get(1).enabled); + assertEquals(compact.get(1).lockTime, LocalTime.of(19, 00, 00)); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceTest.java new file mode 100644 index 00000000000..baef0ffee1a --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareDeviceTest.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.text.ParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareDevice; + +/** + * The {@link SurePetcareDeviceTest} class implements unit test case for {@link SurePetcareDevice} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareDeviceTest { + + @Test + public void testJsonDeserializeHub1() throws ParseException { + String testResponse = "{\"id\":296464,\"product_id\":1,\"household_id\":48712,\"name\":\"Home Hub\",\"serial_number\":\"H008-0296432\",\"mac_address\":\"00000491630A0D64\",\"version\":\"NjA=\",\"created_at\":\"2019-04-18T14:45:11+00:00\",\"updated_at\":\"2019-09-30T12:31:52+00:00\",\"control\":{\"led_mode\":4,\"pairing_mode\":0},\"status\":{\"led_mode\":4,\"pairing_mode\":0,\"version\":{\"device\":{\"hardware\":3,\"firmware\":1.772}},\"online\":true}}"; + SurePetcareDevice response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareDevice.class); + + if (response != null) { + assertEquals(Long.valueOf(296464L), response.id); + assertEquals(Integer.valueOf(1), response.productId); + assertEquals(Long.valueOf(48712), response.householdId); + assertEquals("Home Hub", response.name); + assertEquals("H008-0296432", response.serialNumber); + assertEquals("00000491630A0D64", response.macAddress); + assertEquals("NjA=", response.version); + assertEquals(Integer.valueOf(4), response.control.ledModeId); + assertEquals(Integer.valueOf(0), response.control.pairingModeId); + assertEquals(Integer.valueOf(4), response.status.ledModeId); + assertEquals(Integer.valueOf(0), response.status.pairingModeId); + assertEquals("3", response.status.version.device.hardware); + assertEquals("1.772", response.status.version.device.firmware); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonDeserializeHub2() throws ParseException { + String testResponse = "{\"id\":101797,\"product_id\":1,\"household_id\":21005,\"name\":\"Home Hub\",\"serial_number\":\"H005-0101321\",\"mac_address\":\"0000801F1341F1C7\",\"version\":\"NzAzNg==\",\"created_at\":\"2018-05-18T11:11:59+00:00\",\"updated_at\":\"2020-05-01T07:51:32+00:00\",\"control\":{\"led_mode\":4,\"pairing_mode\":0},\"status\":{\"led_mode\":4,\"pairing_mode\":0,\"version\":{\"device\":{\"hardware\":3,\"firmware\":2.43}},\"online\":true}}"; + SurePetcareDevice response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareDevice.class); + + if (response != null) { + assertEquals(Long.valueOf(101797), response.id); + assertEquals(Integer.valueOf(1), response.productId); + assertEquals(Long.valueOf(21005), response.householdId); + assertEquals("Home Hub", response.name); + assertEquals("H005-0101321", response.serialNumber); + assertEquals("0000801F1341F1C7", response.macAddress); + assertEquals("NzAzNg==", response.version); + assertEquals(Integer.valueOf(4), response.control.ledModeId); + assertEquals(Integer.valueOf(0), response.control.pairingModeId); + assertEquals(Integer.valueOf(4), response.status.ledModeId); + assertEquals(Integer.valueOf(0), response.status.pairingModeId); + assertEquals("3", response.status.version.device.hardware); + assertEquals("2.43", response.status.version.device.firmware); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonDeserializeCatFlap() throws ParseException { + String testResponse = "{\"id\":318966,\"parent_device_id\":296464,\"product_id\":6,\"household_id\":48712,\"name\":\"Back Door Cat Flap\",\"serial_number\":\"N005-0089709\",\"mac_address\":\"6D5E01CFF9D5B370\",\"index\":0,\"version\":\"MTE5\",\"created_at\":\"2019-05-13T14:09:18+00:00\",\"updated_at\":\"2019-10-01T07:37:20+00:00\",\"pairing_at\":\"2019-09-02T08:24:13+00:00\",\"control\":{\"curfew\":[{\"enabled\":true,\"lock_time\":\"19:30\",\"unlock_time\":\"07:00\"}],\"locking\":0,\"fast_polling\":false},\"parent\":{\"id\":296464,\"product_id\":1,\"household_id\":48712,\"name\":\"Home Hub\",\"serial_number\":\"H008-0296464\",\"mac_address\":\"00000491620A0F60\",\"version\":\"NjE=\",\"created_at\":\"2019-04-18T14:45:11+00:00\",\"updated_at\":\"2019-10-01T07:37:20+00:00\"},\"status\":{\"locking\":{\"mode\":0},\"version\":{\"device\":{\"hardware\":9,\"firmware\":335}},\"battery\":5.771,\"learn_mode\":null,\"online\":true,\"signal\":{\"device_rssi\":-87.25,\"hub_rssi\":-83.5}},\"tags\":[{\"id\":60456,\"index\":0,\"profile\":2,\"version\":\"MA==\",\"created_at\":\"2019-09-02T09:27:17+00:00\",\"updated_at\":\"2019-09-02T09:27:23+00:00\"}]}"; + SurePetcareDevice response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareDevice.class); + + if (response != null) { + response.getThingProperties(); + assertEquals(Long.valueOf(318966), response.id); + assertEquals(Integer.valueOf(6), response.productId); + assertEquals(Long.valueOf(48712), response.householdId); + assertEquals("Back Door Cat Flap", response.name); + assertEquals("N005-0089709", response.serialNumber); + assertEquals("6D5E01CFF9D5B370", response.macAddress); + assertEquals("9", response.status.version.device.hardware); + assertEquals("335", response.status.version.device.firmware); + assertEquals("MTE5", response.version); + assertEquals(Integer.valueOf(0), response.status.locking.modeId); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonDeserializePetFlap() throws ParseException { + String testResponse = "{\"id\":318966,\"parent_device_id\":296464,\"product_id\":3,\"household_id\":48712,\"name\":\"Back Door Cat Flap\",\"mac_address\":\"6D5E01CFF9D5B370\",\"index\":0,\"version\":\"MjYxMw==\",\"created_at\":\"2019-03-02T14:54:30+00:00\",\"updated_at\":\"2020-05-01T07:51:32+00:00\",\"pairing_at\":\"2019-06-18T19:54:34+00:00\",\"control\":{\"curfew\":{\"enabled\":true,\"lock_time\":\"19:00\",\"unlock_time\":\"08:00\"},\"fast_polling\":true},\"parent\":{\"id\":101797,\"product_id\":1,\"household_id\":21005,\"name\":\"Salem\",\"serial_number\":\"N005-0089709\",\"mac_address\":\"0000801F1221F1C2\",\"version\":\"NzAzNg==\",\"created_at\":\"2018-05-18T11:11:59+00:00\",\"updated_at\":\"2020-05-01T07:51:32+00:00\"},\"status\":{\"battery\":5.864999999999999,\"locking\":{\"mode\":4,\"curfew\":{\"delay_time\":0,\"lock_time\":\"19:00\",\"permission\":2,\"unlock_time\":\"08:00\",\"locked\":false}},\"version\":{\"lcd\":{\"hardware\":1,\"firmware\":1},\"rf\":{\"hardware\":4,\"firmware\":0.16}},\"learn_mode\":false,\"online\":true,\"signal\":{\"device_rssi\":-88.33333333333333,\"hub_rssi\":-86}},\"tags\":[{\"id\":24725,\"index\":0,\"version\":\"MA==\",\"created_at\":\"2019-06-18T19:54:42+00:00\",\"updated_at\":\"2020-03-11T16:06:58+00:00\"}]}"; + SurePetcareDevice response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareDevice.class); + + if (response != null) { + response.getThingProperties(); + assertEquals(Long.valueOf(318966), response.id); + assertEquals(Integer.valueOf(3), response.productId); + assertEquals(Long.valueOf(48712), response.householdId); + assertEquals("Back Door Cat Flap", response.name); + assertNull(response.serialNumber); + assertEquals("6D5E01CFF9D5B370", response.macAddress); + assertEquals("1", response.status.version.lcd.hardware); + assertEquals("1", response.status.version.lcd.firmware); + assertEquals("4", response.status.version.rf.hardware); + assertEquals("0.16", response.status.version.rf.firmware); + assertEquals("MjYxMw==", response.version); + assertEquals(Integer.valueOf(4), response.status.locking.modeId); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareHouseholdTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareHouseholdTest.java new file mode 100644 index 00000000000..1a2d68899b0 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareHouseholdTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.text.ParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareHousehold; + +/** + * The {@link SurePetcareHouseholdTest} class implements unit test case for {@link SurePetcareHousehold} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareHouseholdTest { + + @Test + public void testJsonDeserialize1() throws ParseException { + String testReponse = "{\"id\":2491083182,\"name\":\"Test Home\",\"share_code\":\"BFHvjQ8DgvnP\",\"timezone_id\":340,\"version\":\"MA==\",\"created_at\":\"2019-09-02T08:20:45+00:00\",\"updated_at\":\"2019-09-02T08:20:48+00:00\",\"invites\":[{\"id\":12352,\"code\":\"QDEZHNNHFG\",\"email_address\":\"user1@gugus.com\",\"creator_user_id\":32712,\"acceptor_user_id\":87621,\"owner\":false,\"write\":true,\"status\":1,\"version\":\"Mg==\",\"created_at\":\"2019-09-09T10:33:36+00:00\",\"updated_at\":\"2019-09-09T11:59:39+00:00\",\"user\":{\"acceptor\":{\"id\":87621,\"name\":\"User1\"},\"creator\":{\"id\":32712,\"name\":\"Admin User\"}}}],\"users\":[{\"id\":32712,\"owner\":true,\"write\":true,\"version\":\"MA==\",\"created_at\":\"2019-09-02T08:20:45+00:00\",\"updated_at\":\"2019-09-02T08:20:50+00:00\",\"user\":{\"id\":32712,\"name\":\"Admin User\"}},{\"id\":87621,\"owner\":false,\"write\":true,\"version\":\"MA==\",\"created_at\":\"2019-09-09T11:59:39+00:00\",\"updated_at\":\"2019-09-09T11:59:39+00:00\",\"user\":{\"id\":87621,\"name\":\"User1\"}}]}"; + SurePetcareHousehold response = SurePetcareConstants.GSON.fromJson(testReponse, SurePetcareHousehold.class); + + if (response != null) { + assertEquals(Long.valueOf(2491083182L), response.id); + assertEquals("Test Home", response.name); + assertEquals("BFHvjQ8DgvnP", response.shareCode); + assertEquals(Integer.valueOf(340), response.timezoneId); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonDeserialize2() throws ParseException { + String testReponse = "{\"id\":2491083182,\"name\":\"Test Home\",\"share_code\":\"BFHvjQ8DgvnP\",\"timezone_id\":320,\"version\":\"MQ==\",\"created_at\":\"2018-12-21T17:50:07+00:00\",\"updated_at\":\"2018-12-21T17:50:07+00:00\",\"invites\":[{\"id\":10414,\"code\":\"KHDSUYRBKJF\",\"email_address\":\"test@gmx.de\",\"creator_user_id\":22360,\"owner\":false,\"write\":false,\"status\":0,\"version\":\"MA==\",\"created_at\":\"2018-12-22T09:15:10+00:00\",\"updated_at\":\"2018-12-22T09:15:10+00:00\",\"user\":{\"creator\":{\"id\":22360,\"name\":\"Owner Name\"}}}],\"users\":[{\"id\":22360,\"owner\":true,\"write\":true,\"version\":\"MQ==\",\"created_at\":\"2018-12-21T17:50:07+00:00\",\"updated_at\":\"2018-12-21T17:50:13+00:00\",\"user\":{\"id\":22360,\"name\":\"Owner Name\"}}]}"; + SurePetcareHousehold response = SurePetcareConstants.GSON.fromJson(testReponse, SurePetcareHousehold.class); + + if (response != null) { + assertEquals(Long.valueOf(2491083182L), response.id); + assertEquals("Test Home", response.name); + assertEquals("BFHvjQ8DgvnP", response.shareCode); + assertEquals(Integer.valueOf(320), response.timezoneId); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcarePetActivityTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcarePetActivityTest.java new file mode 100644 index 00000000000..5a7cdf73dd6 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcarePetActivityTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.text.ParseException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePetActivity; + +/** + * The {@link SurePetcarePetLocationTest} class implements unit test case for {@link SurePetcarePetLocation} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcarePetActivityTest { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + @Test + public void testJsonDeserialize() throws ParseException { + String testReponse = "{\"tag_id\":60126,\"device_id\":376236,\"where\":2,\"since\":\"2019-09-11T13:09:07+00:00\"}"; + SurePetcarePetActivity response = SurePetcareConstants.GSON.fromJson(testReponse, SurePetcarePetActivity.class); + + if (response != null) { + assertEquals(Long.valueOf(60126), response.tagId); + assertEquals(Long.valueOf(376236), response.deviceId); + assertEquals(Integer.valueOf(2), response.where); + ZonedDateTime since = FORMATTER.parse("2019-09-11T13:09:07+00:00", ZonedDateTime::from); + assertEquals(since, response.since); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonFullSerialize() throws ParseException { + ZonedDateTime since = ZonedDateTime.parse("2019-09-11T13:09:07+00:00"); + SurePetcarePetActivity location = new SurePetcarePetActivity(2, since); + + String json = SurePetcareConstants.GSON.toJson(location, SurePetcarePetActivity.class); + + assertEquals("{\"where\":2,\"since\":\"2019-09-11T13:09:07+00:00\"}", json); + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcarePetTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcarePetTest.java new file mode 100644 index 00000000000..8dc6e6851f3 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcarePetTest.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcarePet; + +/** + * The {@link SurePetcarePetTest} class implements unit test case for {@link SurePetcarePet} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcarePetTest { + + // { + // "id":34675, + // "name":"Cat", + // "gender":0, + // "date_of_birth":"2017-08-01T00:00:00+00:00", + // "weight":"3.5", + // "comments":"Test Comment", + // "household_id":87435, + // "breed_id":382, + // "photo_id":23412, + // "species_id":1, + // "tag_id":60456, + // "version":"Mw==", + // "created_at":"2019-09-02T09:27:17+00:00", + // "updated_at":"2019-10-03T12:17:48+00:00", + // "conditions":[ + // { + // "id":18, + // "version":"MA==", + // "created_at":"2019-10-03T12:17:48+00:00", + // "updated_at":"2019-10-03T12:17:48+00:00" + // }, + // { + // "id":17, + // "version":"MA==", + // "created_at":"2019-10-03T12:17:48+00:00", + // "updated_at":"2019-10-03T12:17:48+00:00" + // } + // ], + // "photo":{ + // "id":79293, + // "location":"https:\/\/surehub.s3.amazonaws.com\/user-photos\/thm\/23412\/z70LUtqaHVhlsdfuyHKJH5HDysg5AR6GvQwdAZptCgeZU.jpg", + // "uploading_user_id":52815, + // "version":"MA==", + // "created_at":"2019-09-02T09:31:07+00:00", + // "updated_at":"2019-09-02T09:31:07+00:00" + // }, + // "position":{ + // "tag_id":60456, + // "device_id":318986, + // "where":1, + // "since":"2019-10-03T10:23:37+00:00" + // }, + // "status":{ + // "activity":{ + // "tag_id":60456, + // "device_id":318986, + // "where":1, + // "since":"2019-10-03T10:23:37+00:00" + // } + // } + // } + + private static final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter ZONED_DATETIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + @Test + public void testJsonDeserialize1() throws ParseException { + String testReponse = "{\"id\":34675,\"name\":\"Cat\",\"gender\":0,\"date_of_birth\":\"2017-08-01T00:00:00+00:00\",\"weight\":\"3.5\",\"comments\":\"Test Comment\",\"household_id\":87435,\"breed_id\":382,\"photo_id\":23412,\"species_id\":1,\"tag_id\":60456,\"version\":\"Mw==\",\"created_at\":\"2019-09-02T09:27:17+00:00\",\"updated_at\":\"2019-10-03T12:17:48+00:00\",\"conditions\":[{\"id\":18,\"version\":\"MA==\",\"created_at\":\"2019-10-03T12:17:48+00:00\",\"updated_at\":\"2019-10-03T12:17:48+00:00\"},{\"id\":17,\"version\":\"MA==\",\"created_at\":\"2019-10-03T12:17:48+00:00\",\"updated_at\":\"2019-10-03T12:17:48+00:00\"}],\"photo\":{\"id\":79293,\"location\":\"https:\\/\\/surehub.s3.amazonaws.com\\/user-photos\\/thm\\/23412\\/z70LUtqaHVhlsdfuyHKJH5HDysg5AR6GvQwdAZptCgeZU.jpg\",\"uploading_user_id\":52815,\"version\":\"MA==\",\"created_at\":\"2019-09-02T09:31:07+00:00\",\"updated_at\":\"2019-09-02T09:31:07+00:00\"},\"position\":{\"tag_id\":60456,\"device_id\":318986,\"where\":1,\"since\":\"2019-10-03T10:23:37+00:00\"},\"status\":{\"activity\":{\"tag_id\":60456,\"device_id\":318986,\"where\":1,\"since\":\"2019-10-03T10:23:37+00:00\"}}}"; + SurePetcarePet response = SurePetcareConstants.GSON.fromJson(testReponse, SurePetcarePet.class); + + if (response != null) { + assertEquals(Long.valueOf(34675), response.id); + assertEquals("Cat", response.name); + assertEquals(Integer.valueOf(0), response.genderId); + assertEquals(LocalDate.parse("2017-08-01", LOCAL_DATE_FORMATTER), response.dateOfBirth); + assertEquals(BigDecimal.valueOf(3.5), response.weight); + assertEquals("Test Comment", response.comments); + assertEquals(Long.valueOf(87435), response.householdId); + assertEquals(Long.valueOf(23412), response.photoId); + assertEquals(SurePetcarePet.PetSpecies.CAT.id, response.speciesId); + assertEquals(Integer.valueOf(382), response.breedId); + assertEquals(Integer.valueOf(1), response.status.activity.where); + assertEquals(ZonedDateTime.parse("2019-10-03T10:23:37+00:00", ZONED_DATETIME_FORMATTER), + response.status.activity.since); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testJsonDeserialize2() throws ParseException { + String testReponse = "{\"id\":30622,\"name\":\"Cat\",\"gender\":1,\"date_of_birth\":\"2016-04-01T00:00:00+00:00\",\"weight\":\"6\",\"comments\":\"\",\"household_id\":21005,\"breed_id\":382,\"food_type_id\":1,\"photo_id\":77957,\"species_id\":1,\"tag_id\":24725,\"version\":\"OA==\",\"created_at\":\"2018-12-22T08:59:13+00:00\",\"updated_at\":\"2019-08-26T18:17:38+00:00\",\"photo\":{\"id\":77957,\"location\":\"https://surehub.s3.amazonaws.com/user-photos/thm/22360/1jhp4OtwmNvWXrsT4pWLJhoYOt7Ti9UVm5SjsFoC9Y.jpg \",\"uploading_user_id\":22360,\"version\":\"MA==\",\"created_at\":\"2019-08-26T18:17:38+00:00\",\"updated_at\":\"2019-08-26T18:17:38+00:00\"},\"position\":{\"tag_id\":24725,\"device_id\":243573,\"where\":2,\"since\":\"2020-05-01T06:01:53+00:00\"},\"status\":{\"activity\":{\"tag_id\":24725,\"device_id\":243573,\"where\":2,\"since\":\"2020-05-01T06:01:53+00:00\"}}}"; + SurePetcarePet response = SurePetcareConstants.GSON.fromJson(testReponse, SurePetcarePet.class); + + if (response != null) { + assertEquals(Long.valueOf(30622), response.id); + assertEquals("Cat", response.name); + assertEquals(Integer.valueOf(1), response.genderId); + assertEquals(LocalDate.parse("2016-04-01", LOCAL_DATE_FORMATTER), response.dateOfBirth); + assertEquals(BigDecimal.valueOf(6), response.weight); + assertEquals("", response.comments); + assertEquals(Long.valueOf(21005), response.householdId); + assertEquals(Long.valueOf(77957), response.photoId); + assertEquals(SurePetcarePet.PetSpecies.CAT.id, response.speciesId); + assertEquals(Integer.valueOf(382), response.breedId); + assertEquals(Integer.valueOf(2), response.status.activity.where); + assertEquals(ZonedDateTime.parse("2020-05-01T06:01:53+00:00", ZONED_DATETIME_FORMATTER), + response.status.activity.since); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareTopologyTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareTopologyTest.java new file mode 100644 index 00000000000..a1b3e933a75 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/data/SurePetcareTopologyTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareTopology; + +/** + * The {@link SurePetcareTopologyTest} class implements unit test case for {@link SurePetcareTopology} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareTopologyTest { + + @Test + public void testTopologyPopulated() { + String testResponse = "{\"devices\":[{\"id\":23912},{\"id\":23481}],\"households\":[{\"id\":83271}],\"pets\":[{\"id\":12345}],\"photos\":[{\"id\":64257,\"version\":\"MA==\",\"created_at\":\"2019-10-04T16:03:20+00:00\",\"updated_at\":\"2019-10-04T16:03:20+00:00\"}],\"user\":{\"id\":33421,\"version\":\"MA==\",\"created_at\":\"2019-09-18T16:09:30+00:00\",\"updated_at\":\"2019-09-18T16:09:30+00:00\"}}"; + SurePetcareTopology response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareTopology.class); + + if (response != null) { + assertNotNull(response.devices); + assertEquals(2, response.devices.size()); + assertNotNull(response.households); + assertEquals(1, response.households.size()); + assertNotNull(response.pets); + assertEquals(1, response.pets.size()); + assertNotNull(response.photos); + assertEquals(1, response.photos.size()); + assertNotNull(response.user); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testTopologyEmpty() { + String testResponse = "{}"; + SurePetcareTopology response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareTopology.class); + + if (response != null) { + assertNotNull(response.tags); + assertEquals(0, response.tags.size()); + assertNotNull(response.households); + assertEquals(0, response.households.size()); + assertNotNull(response.pets); + assertEquals(0, response.pets.size()); + assertNotNull(response.devices); + assertEquals(0, response.devices.size()); + assertNull(response.user); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testGetUserNull() { + String testResponse = "{\"devices\":[{\"id\":23912},{\"id\":23481}],\"households\":[{\"id\":83271}],\"pets\":[{\"id\":12345}],\"photos\":[{\"id\":64257,\"version\":\"MA==\",\"created_at\":\"2019-10-04T16:03:20+00:00\",\"updated_at\":\"2019-10-04T16:03:20+00:00\"}]}"; + SurePetcareTopology response = SurePetcareConstants.GSON.fromJson(testResponse, SurePetcareTopology.class); + + if (response != null) { + assertNull(response.user); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareLoginResponseTest.java b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareLoginResponseTest.java new file mode 100644 index 00000000000..22e789e5c63 --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/java/org/openhab/binding/surepetcare/internal/handler/SurePetcareLoginResponseTest.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2021 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.surepetcare.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.surepetcare.internal.SurePetcareConstants; +import org.openhab.binding.surepetcare.internal.dto.SurePetcareLoginResponse; + +/** + * The {@link SurePetcareLoginResponseTest} class implements unit test case for {@link SurePetcareLoginResponse} + * + * @author Rene Scherer - Initial contribution + */ +@NonNullByDefault +public class SurePetcareLoginResponseTest { + + @Test + public void testParseLoginResponse() { + String testReponse = "{\"data\":{\"user\":{\"id\":23412,\"email_address\":\"rene@gugus.com\",\"first_name\":\"Rene\",\"last_name\":\"Scherer\",\"country_id\":77,\"language_id\":37,\"marketing_opt_in\":false,\"terms_accepted\":true,\"weight_units\":0,\"time_format\":0,\"version\":\"MA==\",\"created_at\":\"2019-09-02T08:20:03+00:00\",\"updated_at\":\"2019-09-02T08:20:03+00:00\",\"notifications\":{\"device_status\":true,\"animal_movement\":true,\"intruder_movements\":true,\"new_device_pet\":true,\"household_management\":true,\"photos\":true,\"low_battery\":true,\"curfew\":true,\"feeding_activity\":true}},\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcC5hcGkuc3VyZWh1Yi5pby9hcGkvYXV0aC9sb2dpbiIsImlhdCI6MTU2NzYxMjY2OSwiZXhwIjoxNTk5MDYyMjY5LCJuYmYiOjE1Njc2MTI2NjksImp0aSI6IlY4M1lJQlJ5dVRqMUVDcWsiLCJzdWIiOjUyODE1LCJwcnYiOiJiM2VkM2RiMzM0YzJiYzMzYjE4NDI2OTQ3NTU3NTZhM2ZmYmY1YTdkIiwiZGV2aWNlX2lkIjoiNTczODc2MzQifQ.WeRutm8I7gMb21dtrknDh6LGFkwxfrXcak-IoykwvV8\"}}"; + SurePetcareLoginResponse response = SurePetcareConstants.GSON.fromJson(testReponse, + SurePetcareLoginResponse.class); + if (response != null) { + assertEquals("Rene", response.data.user.firstName); + assertEquals(363, response.data.token.length()); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.surepetcare/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/bundles/org.openhab.binding.surepetcare/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000000..1f0955d450f --- /dev/null +++ b/bundles/org.openhab.binding.surepetcare/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/bundles/pom.xml b/bundles/pom.xml index 4f38c9a0194..3c6b37c4875 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -289,6 +289,7 @@ org.openhab.binding.sonyprojector org.openhab.binding.spotify org.openhab.binding.squeezebox + org.openhab.binding.surepetcare org.openhab.binding.synopanalyzer org.openhab.binding.systeminfo org.openhab.binding.tacmi