Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions custom_components/taskmate/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def is_chore_available_for_child(self, chore, child_id: str) -> bool:

try:
last_dt = date.fromisoformat(current_iso[:10])
except ValueError:
except (ValueError, TypeError):
return True

# every_2_days with anchor — check alignment
Expand Down Expand Up @@ -676,10 +676,7 @@ async def async_approve_reward(self, claim_id: str) -> None:

async def async_reject_reward(self, claim_id: str) -> None:
"""Reject a reward claim — no refund needed as points were never deducted."""
self.storage._data["reward_claims"] = [
c for c in self.storage._data.get("reward_claims", [])
if c.get("id") != claim_id
]
self.storage.remove_reward_claim(claim_id)
await self.storage.async_save()
await self.async_refresh()

Expand Down
22 changes: 19 additions & 3 deletions custom_components/taskmate/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@
_LOGGER = logging.getLogger(__name__)


def _safe_float(value, default: float) -> float:
"""Convert value to float, returning default on failure."""
try:
return float(value) if value is not None else default
except (ValueError, TypeError):
return default


def _safe_int(value, default: int) -> int:
"""Convert value to int, returning default on failure."""
try:
return int(value) if value is not None else default
except (ValueError, TypeError):
return default


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
Expand Down Expand Up @@ -147,7 +163,7 @@ def extra_state_attributes(self) -> dict:
"completion_id": comp.id,
"chore_id": comp.chore_id,
"child_id": comp.child_id,
"child_name": child_lookup.get(comp.child_id, None) and child_lookup[comp.child_id].name or "",
"child_name": child_lookup[comp.child_id].name if comp.child_id in child_lookup else "",
"chore_name": matched_chore.name if matched_chore else "",
"points": matched_chore.points if matched_chore else 0,
"approved": comp.approved,
Expand Down Expand Up @@ -231,11 +247,11 @@ def extra_state_attributes(self) -> dict:
return {
"today_day_of_week": today_dow,
"streak_reset_mode": data.get("settings", {}).get("streak_reset_mode", "reset"),
"weekend_multiplier": float(data.get("settings", {}).get("weekend_multiplier", "2.0") or "2.0"),
"weekend_multiplier": _safe_float(data.get("settings", {}).get("weekend_multiplier"), 2.0),
"streak_milestones_enabled": data.get("settings", {}).get("streak_milestones_enabled", "true") == "true",
"streak_milestones": data.get("settings", {}).get("streak_milestones", "3:5, 7:10, 14:20, 30:50, 60:100, 100:200"),
"perfect_week_enabled": data.get("settings", {}).get("perfect_week_enabled", "true") == "true",
"perfect_week_bonus": int(data.get("settings", {}).get("perfect_week_bonus", "50") or "50"),
"perfect_week_bonus": _safe_int(data.get("settings", {}).get("perfect_week_bonus"), 50),
"total_children": len(children),
"total_chores": len(chores),
"total_rewards": len(rewards),
Expand Down
6 changes: 6 additions & 0 deletions custom_components/taskmate/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ def update_reward_claim(self, claim: RewardClaim) -> None:
claims[i] = claim.to_dict()
return

def remove_reward_claim(self, claim_id: str) -> None:
"""Remove a reward claim."""
self._data["reward_claims"] = [
c for c in self._data.get("reward_claims", []) if c.get("id") != claim_id
]

# Points transactions management
def get_points_transactions(self) -> list[PointsTransaction]:
"""Get all points transactions."""
Expand Down
6 changes: 6 additions & 0 deletions tests/test_coordinator_rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ def _make_coord(*, children=None, rewards=None, claims=None):
storage.async_save = AsyncMock()
storage._data = {"reward_claims": [c.to_dict() for c in _claims]}

def _remove_reward_claim(claim_id):
storage._data["reward_claims"] = [
c for c in storage._data["reward_claims"] if c.get("id") != claim_id
]
storage.remove_reward_claim = MagicMock(side_effect=_remove_reward_claim)

coord.storage = storage
coord.async_refresh = AsyncMock()
return coord
Expand Down
Loading