Skip to content

Commit 64728d4

Browse files
committed
Merge branch 'dev' into fix/upstream-140
2 parents fcb48e2 + a16baf2 commit 64728d4

27 files changed

Lines changed: 840 additions & 48 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ logger:
133133
---
134134
## Star History
135135

136-
[![Star History Chart](https://api.star-history.com/svg?repos=jjjonesjr33/petlibro&type=Date&theme=dark)](https://www.star-history.com/#jjjonesjr33/petlibro&Date)
136+
[![Star History Chart](https://star-history.dera.page/svg?repos=jjjonesjr33/petlibro&type=Date&theme=dark)](https://star-history.dera.page/#jjjonesjr33/petlibro&Date)
137137

138138
[stars]: https://github.com/jjjonesjr33/petlibro/stargazers
139139
[starsbadge]: https://img.shields.io/github/stars/jjjonesjr33/petlibro?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBzdHlsZT0iZmlsbDojY2NjY2NjIiBkPSJNOCAuMjVhLjc1Ljc1IDAgMCAxIC42NzMuNDE4bDEuODgyIDMuODE1IDQuMjEuNjEyYS43NS43NSAwIDAgMSAuNDE2IDEuMjc5bC0zLjA0NiAyLjk3LjcxOSA0LjE5MmEuNzUxLjc1MSAwIDAgMS0xLjA4OC43OTFMOCAxMi4zNDdsLTMuNzY2IDEuOThhLjc1Ljc1IDAgMCAxLTEuMDg4LS43OWwuNzItNC4xOTRMLjgxOCA2LjM3NGEuNzUuNzUgMCAwIDEgLjQxNi0xLjI4bDQuMjEtLjYxMUw3LjMyNy42NjhBLjc1Ljc1IDAgMCAxIDggLjI1Wm0wIDIuNDQ1TDYuNjE1IDUuNWEuNzUuNzUgMCAwIDEtLjU2NC40MWwtMy4wOTcuNDUgMi4yNCAyLjE4NGEuNzUuNzUgMCAwIDEgLjIxNi42NjRsLS41MjggMy4wODQgMi43NjktMS40NTZhLjc1Ljc1IDAgMCAxIC42OTggMGwyLjc3IDEuNDU2LS41My0zLjA4NGEuNzUuNzUgMCAwIDEgLjIxNi0uNjY0bDIuMjQtMi4xODMtMy4wOTYtLjQ1YS43NS43NSAwIDAgMS0uNTY0LS40MUw4IDIuNjk0WiI+PC9wYXRoPjwvc3ZnPg==&label=Stars&color=ffffff

custom_components/petlibro/api.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@
2727
import aiohttp
2828
import uuid # To generate unique request IDs
2929

30-
async def make_api_call(session, url, data):
31-
async with session.post(url, json=data) as response:
32-
return await response.json()
33-
3430
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
3531
_LOGGER = getLogger(__name__)
3632

@@ -85,7 +81,7 @@ async def request(self, method: str, url: str, **kwargs: Any) -> JSON:
8581

8682
if self.token is not None:
8783
kwargs["headers"]["token"] = self.token
88-
_LOGGER.debug(f"Using token: {self.token}")
84+
_LOGGER.debug("Using token from config entry")
8985
else:
9086
_LOGGER.warning("No token available for request. Attempting to log in...")
9187

@@ -238,7 +234,7 @@ async def login(self, email: str, password: str) -> str:
238234
raise PetLibroAPIError("No token found during login.")
239235

240236
self.session.token = data["token"]
241-
_LOGGER.debug(f"Login successful, token: {self.session.token}")
237+
_LOGGER.debug("Login successful")
242238
return self.session.token
243239

244240
except Exception as e:

custom_components/petlibro/binary_sensor.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Support for PETLIBRO binary sensors."""
22
from __future__ import annotations
33
import json
4-
from .api import make_api_call
54
import aiohttp
65
from aiohttp import ClientSession, ClientError
76
from dataclasses import dataclass
@@ -230,6 +229,14 @@ def extra_state_attributes(self):
230229
value_fn=lambda device: device.feeding_plan_state,
231230
name="Feeding Schedule"
232231
),
232+
PetLibroBinarySensorEntityDescription[AirSmartFeeder](
233+
key="power_connected",
234+
translation_key="power_connected",
235+
icon="mdi:power-plug",
236+
device_class=BinarySensorDeviceClass.PLUG,
237+
should_report=lambda device: device.power_connected is not None,
238+
name="Power Connected"
239+
),
233240
],
234241
GranarySmartFeeder: [
235242
PetLibroBinarySensorEntityDescription[GranarySmartFeeder](
@@ -294,6 +301,22 @@ def extra_state_attributes(self):
294301
value_fn=lambda device: device.feeding_plan_state,
295302
name="Feeding Schedule"
296303
),
304+
PetLibroBinarySensorEntityDescription[GranarySmartFeeder](
305+
key="left_food_low",
306+
translation_key="left_food_low",
307+
icon="mdi:bowl-mix-outline",
308+
device_class=BinarySensorDeviceClass.PROBLEM,
309+
should_report=lambda device: device.left_food_low is not None,
310+
name="Left Food Status"
311+
),
312+
PetLibroBinarySensorEntityDescription[GranarySmartFeeder](
313+
key="right_food_low",
314+
translation_key="right_food_low",
315+
icon="mdi:bowl-mix-outline",
316+
device_class=BinarySensorDeviceClass.PROBLEM,
317+
should_report=lambda device: device.right_food_low is not None,
318+
name="Right Food Status"
319+
),
297320
],
298321
GranarySmartCameraFeeder: [
299322
PetLibroBinarySensorEntityDescription[GranarySmartCameraFeeder](
@@ -358,6 +381,22 @@ def extra_state_attributes(self):
358381
value_fn=lambda device: device.feeding_plan_state,
359382
name="Feeding Schedule"
360383
),
384+
PetLibroBinarySensorEntityDescription[GranarySmartCameraFeeder](
385+
key="motion_detected",
386+
translation_key="motion_detected",
387+
icon="mdi:motion-sensor",
388+
device_class=BinarySensorDeviceClass.MOTION,
389+
should_report=lambda device: device.motion_detected is not None,
390+
name="Motion Detected"
391+
),
392+
PetLibroBinarySensorEntityDescription[GranarySmartCameraFeeder](
393+
key="sound_detected",
394+
translation_key="sound_detected",
395+
icon="mdi:ear-hearing",
396+
device_class=BinarySensorDeviceClass.SOUND,
397+
should_report=lambda device: device.sound_detected is not None,
398+
name="Sound Detected"
399+
),
361400
],
362401
OneRFIDSmartFeeder: [
363402
PetLibroBinarySensorEntityDescription[OneRFIDSmartFeeder](
@@ -453,6 +492,14 @@ def extra_state_attributes(self):
453492
value_fn=lambda device: device.feeding_plan_state,
454493
name="Feeding Schedule"
455494
),
495+
PetLibroBinarySensorEntityDescription[OneRFIDSmartFeeder](
496+
key="rotor_stuck",
497+
translation_key="rotor_stuck",
498+
icon="mdi:alert",
499+
device_class=BinarySensorDeviceClass.PROBLEM,
500+
should_report=lambda device: device.rotor_stuck is not None,
501+
name="Rotor Status"
502+
),
456503
],
457504
PolarWetFoodFeeder: [
458505
PetLibroBinarySensorEntityDescription[PolarWetFoodFeeder](
@@ -613,6 +660,14 @@ def extra_state_attributes(self):
613660
should_report=lambda device: device.light_switch is not None,
614661
name="Indicator"
615662
),
663+
PetLibroBinarySensorEntityDescription[DockstreamSmartFountain](
664+
key="weight_calibration_error",
665+
translation_key="weight_calibration_error",
666+
icon="mdi:alert",
667+
device_class=BinarySensorDeviceClass.PROBLEM,
668+
should_report=lambda device: device.weight_calibration_error is not None,
669+
name="Weight Calibration Error"
670+
),
616671
],
617672
DockstreamSmartRFIDFountain: [
618673
PetLibroBinarySensorEntityDescription[DockstreamSmartRFIDFountain](

custom_components/petlibro/button.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Support for PETLIBRO buttons."""
22
from __future__ import annotations
33
import re
4-
from .api import make_api_call
54
import aiohttp
65
from aiohttp import ClientSession, ClientError
76
from collections.abc import Callable, Coroutine
@@ -657,7 +656,14 @@ class PetLibroButtonEntityDescription(ButtonEntityDescription, PetLibroEntityDes
657656
translation_key="filter_reset",
658657
set_fn=lambda device: device.set_filter_reset(),
659658
name="Filter Reset"
660-
)
659+
),
660+
PetLibroButtonEntityDescription[DockstreamSmartFountain](
661+
key="calibrate_weight",
662+
translation_key="calibrate_weight",
663+
icon="mdi:scale",
664+
set_fn=lambda device: device.calibrate_weight(),
665+
name="Calibrate Weight Sensor"
666+
),
661667
],
662668
DockstreamSmartRFIDFountain: [
663669
PetLibroButtonEntityDescription[DockstreamSmartRFIDFountain](

custom_components/petlibro/config_flow.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,40 @@ async def async_step_reauth_confirm(self, user_input: dict[str, str] | None = No
127127
errors=errors,
128128
)
129129

130+
async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
131+
"""Allow the user to update their email and/or password."""
132+
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
133+
errors: dict[str, str] = {}
134+
135+
if user_input is not None:
136+
self.email = user_input[CONF_EMAIL]
137+
self.password = user_input[CONF_PASSWORD]
138+
self.region = entry.data.get(CONF_REGION, "US")
139+
140+
if not (error := await self._validate_input()):
141+
self.hass.config_entries.async_update_entry(
142+
entry,
143+
title=self.email,
144+
data={
145+
CONF_REGION: self.region,
146+
CONF_EMAIL: self.email,
147+
CONF_PASSWORD: self.password,
148+
CONF_API_TOKEN: self.token,
149+
},
150+
)
151+
await self.hass.config_entries.async_reload(entry.entry_id)
152+
return self.async_abort(reason="reconfigure_successful")
153+
errors["base"] = error
154+
155+
return self.async_show_form(
156+
step_id="reconfigure",
157+
data_schema=vol.Schema({
158+
vol.Required(CONF_EMAIL, default=entry.data.get(CONF_EMAIL, "")): str,
159+
vol.Required(CONF_PASSWORD): str,
160+
}),
161+
errors=errors,
162+
)
163+
130164
async def _validate_input(self) -> str:
131165
"""Validate the user input allows us to connect.
132166
@@ -142,7 +176,7 @@ async def _validate_input(self) -> str:
142176
)
143177

144178
self.token = await api.login(self.email, self.password)
145-
_LOGGER.debug(f"Login successful, token: {self.token}")
179+
_LOGGER.debug("Login successful")
146180
except PetLibroCannotConnect:
147181
return "cannot_connect"
148182
except PetLibroInvalidAuth:
@@ -192,7 +226,7 @@ async def async_step_init(
192226
_LOGGER.debug(
193227
"Started Petlibro options flow for account %s", self.entry.data[CONF_EMAIL]
194228
)
195-
return self.async_show_menu(menu_options=["integration_settings", "account_settings"])
229+
return self.async_show_menu(menu_options=["integration_settings", "account_settings", "change_credentials"])
196230

197231
async def async_step_integration_settings(
198232
self, user_input: dict[str, Any] | None = None
@@ -325,6 +359,54 @@ async def async_step_account_settings(
325359
_LOGGER.debug("Showing account settings form.")
326360
return self._show_account_settings_form(user_input)
327361

362+
async def async_step_change_credentials(
363+
self, user_input: dict[str, Any] | None = None
364+
) -> ConfigFlowResult:
365+
"""Allow the user to update their login email and/or password."""
366+
errors: dict[str, str] = {}
367+
368+
if user_input:
369+
new_email = user_input[CONF_EMAIL]
370+
new_password = user_input[CONF_PASSWORD]
371+
try:
372+
api = PetLibroAPI(
373+
async_get_clientsession(self.hass),
374+
self.hass.config.time_zone,
375+
self.entry.data[CONF_REGION],
376+
new_email,
377+
new_password,
378+
)
379+
new_token = await api.login(new_email, new_password)
380+
except PetLibroInvalidAuth:
381+
errors["base"] = "invalid_auth"
382+
except PetLibroCannotConnect:
383+
errors["base"] = "cannot_connect"
384+
except Exception:
385+
_LOGGER.exception("Unexpected error updating credentials")
386+
errors["base"] = "unknown"
387+
else:
388+
self.hass.config_entries.async_update_entry(
389+
self.entry,
390+
title=new_email,
391+
data={
392+
**self.entry.data,
393+
CONF_EMAIL: new_email,
394+
CONF_PASSWORD: new_password,
395+
CONF_API_TOKEN: new_token,
396+
},
397+
)
398+
await self.hass.config_entries.async_reload(self.entry.entry_id)
399+
return self.async_abort(reason="credentials_updated")
400+
401+
return self.async_show_form(
402+
step_id="change_credentials",
403+
data_schema=vol.Schema({
404+
vol.Required(CONF_EMAIL, default=self.entry.data.get(CONF_EMAIL, "")): str,
405+
vol.Required(CONF_PASSWORD): str,
406+
}),
407+
errors=errors,
408+
)
409+
328410
# ------------------------------
329411
# Form Builders
330412
# ------------------------------

custom_components/petlibro/date.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def async_setup_entry(
5353

5454
# Ensure that the pets are loaded
5555
if not (pets := hub.pets):
56-
_LOGGER.warning("No pets found in hub during date setup.")
56+
_LOGGER.debug("No pets found in hub during date setup.")
5757

5858
if not (pets): # or devices
5959
return

custom_components/petlibro/devices/feeders/air_smart_feeder.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ async def refresh(self):
3434
get_work_record = await self.api.get_device_work_record(self.serial)
3535
feeding_plan_list = (await self.api.device_feeding_plan_list(self.serial)
3636
if self._data.get("enableFeedingPlan") else [])
37+
data_real_info = await self.api.device_data_real_info(self.serial)
3738

38-
# Update internal data with fetched API data
3939
self.update_data({
4040
"grainStatus": grain_status or {},
4141
"realInfo": real_info or {},
42+
"dataRealInfo": data_real_info or {},
4243
"getUpgrade": get_upgrade or {},
4344
"getAttributeSetting": attribute_settings or {},
4445
"getfeedingplantoday": get_feeding_plan_today or {},
@@ -150,6 +151,13 @@ def electric_quantity(self) -> float:
150151
quantity = self._data.get("realInfo", {}).get("electricQuantity")
151152
return quantity if isinstance(quantity, (float, int)) else 0
152153

154+
@property
155+
def power_connected(self) -> bool | None:
156+
power_type = self._data.get("dataRealInfo", {}).get("powerType")
157+
if power_type is None:
158+
return None
159+
return power_type == 3
160+
153161
@property
154162
def enable_feeding_plan(self) -> bool:
155163
return self._data.get("realInfo", {}).get("enableFeedingPlan", False)

custom_components/petlibro/devices/feeders/granary_smart_camera_feeder.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ async def refresh(self):
3131
get_work_record = await self.api.get_device_work_record(self.serial)
3232
feeding_plan_list = (await self.api.device_feeding_plan_list(self.serial)
3333
if self._data.get("enableFeedingPlan") else [])
34+
get_device_events = await self.api.device_events(self.serial)
3435
# Update internal data with fetched API data
3536
self.update_data({
3637
"grainStatus": grain_status or {},
@@ -39,7 +40,8 @@ async def refresh(self):
3940
"getUpgrade": get_upgrade or {},
4041
"getfeedingplantoday": get_feeding_plan_today or {},
4142
"feedingPlan": feeding_plan_list or [],
42-
"workRecord": get_work_record or [],
43+
"workRecord": get_work_record or [],
44+
"getDeviceEvents": get_device_events or {},
4345
})
4446
except PetLibroAPIError as err:
4547
_LOGGER.error(f"Error refreshing data for GranarySmartCameraFeeder: {err}")
@@ -216,6 +218,16 @@ def video_record_switch(self) -> bool:
216218
def video_record_mode(self) -> str:
217219
"""Return the current video recording mode."""
218220
return self._data.get("realInfo", {}).get("videoRecordMode", "unknown")
221+
222+
@property
223+
def motion_detected(self) -> bool:
224+
events = self._data.get("getDeviceEvents", {}).get("data", {}).get("eventInfos", [])
225+
return any(event.get("eventKey") == "MOTION_DETECTED" for event in events)
226+
227+
@property
228+
def sound_detected(self) -> bool:
229+
events = self._data.get("getDeviceEvents", {}).get("data", {}).get("eventInfos", [])
230+
return any(event.get("eventKey") == "SOUND_DETECTED" for event in events)
219231

220232
@property
221233
def remaining_desiccant(self) -> float | None:

0 commit comments

Comments
 (0)