Skip to content

Commit 791b0e5

Browse files
Merge pull request #78 from CamiloValderruten/fix/daily-usage-response-shape
2 parents fd534fd + f42bb08 commit 791b0e5

4 files changed

Lines changed: 181 additions & 105 deletions

File tree

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,18 @@ This repo uses `uv` for Python package dependencies.
9494
2. Run `uv venv`
9595
3. Run `uv sync --dev`
9696
4. Run `./scripts/develop`. This will start Home Assistant locally.
97-
5. Access HAOS `http://localhost:8321`
98-
6. If asked, setup Home Assistant for the first time.
99-
7. Go to Settings > Device & Services > Add Integrations, and create a new FPL integration.
100-
8. Make changes to the source code and restart HAOS to take effect.
97+
5. Access Home Assistant at [http://localhost:8123](http://localhost:8123)
98+
6. If asked, complete the first-time setup (create a local account and password).
99+
7. Go to **Settings****Devices & Services****Add Integration**, and create a new FPL integration.
100+
8. Make changes to the source code and restart `./scripts/develop` to take effect.
101+
102+
### Reset local dev
103+
104+
To wipe your local Home Assistant instance and start over (e.g. you forgot the password):
105+
106+
1. Stop `./scripts/develop` (Ctrl+C in the terminal where it is running).
107+
2. Delete the config directory: `rm -rf config`
108+
3. Run `./scripts/develop` again and complete first-time setup at [http://localhost:8123](http://localhost:8123).
101109

102110
## Contributions are welcome!
103111

custom_components/fpl/FplMainRegionApiClient.py

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,39 @@
1818

1919
STATUS_CATEGORY_OPEN = "OPEN"
2020

21+
22+
def _parse_daily_read_time(read_time):
23+
if not read_time:
24+
return None
25+
if isinstance(read_time, datetime):
26+
return read_time
27+
try:
28+
return datetime.fromisoformat(read_time)
29+
except (TypeError, ValueError):
30+
return None
31+
32+
33+
def _find_daily_usage_row(daily_usage):
34+
daily_rows = daily_usage.get("data") or []
35+
if not daily_rows:
36+
return None
37+
end_date = daily_usage.get("endDate")
38+
if end_date:
39+
for day_usage in daily_rows:
40+
if day_usage.get("date") == end_date:
41+
return day_usage
42+
return daily_rows[-1]
43+
44+
45+
def _usage_block_ok(block):
46+
if not isinstance(block, dict):
47+
return False
48+
exception = block.get("exceptionDetails")
49+
if isinstance(exception, dict) and exception.get("requestStatus") == "Failed":
50+
return False
51+
return True
52+
53+
2154
# URL_LOGIN = API_HOST + "/api/resources/login"
2255
URL_LOGIN = (
2356
API_HOST
@@ -292,10 +325,12 @@ async def get_energy_usage(self, account, premise, lastBilledDate, meterno) -> d
292325

293326
# Tested using MITM proxy and iOS app.
294327
# This is the payload and url used by the iOS app.
328+
# iOS app: status "2" = active/open account, "4" = closed/inactive.
295329
json = {
296330
"status": "2",
297331
"accountType": "RESIDENTIAL",
298332
"premiseNumber": premise,
333+
# account select `currentBillDate` is sent as `lastBilledDate` (MMDDYYYY).
299334
"lastBilledDate": lastBilledDate.strftime("%m%d%Y"),
300335
"amrFlag": "Y",
301336
"revCode": "1",
@@ -319,56 +354,67 @@ async def get_energy_usage(self, account, premise, lastBilledDate, meterno) -> d
319354
)
320355
if response.status == 200:
321356
response_data = await response.json()
322-
json_data = response_data["data"]
323-
324-
current_usage = json_data["CurrentUsage"]
325-
data["projectedKWH"] = int(current_usage.get("projectedKWH"))
326-
data["dailyAverageKWH"] = float(
327-
current_usage.get("dailyAverageKWH")
328-
)
329-
data["billToDate"] = float(current_usage.get("billToDate"))
330-
data["projectedBill"] = float(current_usage.get("projectedBill"))
331-
data["dailyAvg"] = float(current_usage.get("dailyAvg"))
332-
data["avgHighTemp"] = int(current_usage.get("avgHighTemp"))
333-
data["billToDateKWH"] = float(current_usage.get("billToDateKWH"))
334-
data["recMtrReading"] = int(current_usage.get("recMtrReading") or 0)
335-
data["delMtrReading"] = int(current_usage.get("delMtrReading") or 0)
336-
data["billStartDate"] = datetime.strptime(
337-
current_usage.get("billStartDate"), "%m-%d-%Y"
338-
).date()
339-
data["billEndDate"] = datetime.strptime(
340-
current_usage.get("billEndDate"), "%m-%d-%Y"
341-
).date()
342-
343-
daily_usage = json_data["DailyUsage"]
344-
last_day_usage = daily_usage["endDate"]
345-
346-
data["DailyUsage"] = {}
347-
for day_usage in daily_usage["data"]:
348-
# We want to get the last day's usage and use that as the sensor information.
349-
# Given that this sensor should reset every day to the previous day's usage.
350-
if day_usage["date"] == last_day_usage:
351-
data["DailyUsage"]["kwhActual"] = float(
352-
day_usage.get("kwhActual") or 0
353-
)
354-
data["DailyUsage"]["billingCharge"] = float(
355-
day_usage.get("billingCharge") or 0
356-
)
357-
data["DailyUsage"]["readTime"] = datetime.fromisoformat(
358-
day_usage.get("readTime")
357+
json_data = response_data.get("data") or {}
358+
359+
current_usage = json_data.get("CurrentUsage") or {}
360+
if _usage_block_ok(current_usage):
361+
if current_usage.get("projectedKWH") is not None:
362+
data["projectedKWH"] = int(current_usage["projectedKWH"])
363+
if current_usage.get("dailyAverageKWH") is not None:
364+
data["dailyAverageKWH"] = float(
365+
current_usage["dailyAverageKWH"]
359366
)
360-
data["DailyUsage"]["reading"] = float(
361-
day_usage.get("reading")
367+
if current_usage.get("billToDate") is not None:
368+
data["billToDate"] = float(current_usage["billToDate"])
369+
if current_usage.get("projectedBill") is not None:
370+
data["projectedBill"] = float(
371+
current_usage["projectedBill"]
362372
)
363-
364-
# This is most likely not going to work, as this endpoint does not give any information related to delivery metrics.
365-
# TODO: Figure out where the delivery metrics can be grabbed from.
366-
data["DailyUsage"]["netDeliveredKwh"] = float(
367-
day_usage.get("netDeliveredKwh") or 0
373+
if current_usage.get("dailyAvg") is not None:
374+
data["dailyAvg"] = float(current_usage["dailyAvg"])
375+
if current_usage.get("avgHighTemp") is not None:
376+
data["avgHighTemp"] = int(current_usage["avgHighTemp"])
377+
if current_usage.get("billToDateKWH") is not None:
378+
data["billToDateKWH"] = float(
379+
current_usage["billToDateKWH"]
368380
)
369-
data["DailyUsage"]["netDeliveredReading"] = float(
370-
day_usage.get("netDeliveredReading") or 0
381+
data["recMtrReading"] = int(
382+
current_usage.get("recMtrReading") or 0
383+
)
384+
data["delMtrReading"] = int(
385+
current_usage.get("delMtrReading") or 0
386+
)
387+
if current_usage.get("billStartDate"):
388+
data["billStartDate"] = datetime.strptime(
389+
current_usage["billStartDate"], "%m-%d-%Y"
390+
).date()
391+
if current_usage.get("billEndDate"):
392+
data["billEndDate"] = datetime.strptime(
393+
current_usage["billEndDate"], "%m-%d-%Y"
394+
).date()
395+
396+
daily_usage = json_data.get("DailyUsage") or {}
397+
if _usage_block_ok(daily_usage):
398+
day_usage = _find_daily_usage_row(daily_usage)
399+
if day_usage:
400+
read_time = _parse_daily_read_time(
401+
day_usage.get("readTime")
371402
)
403+
if read_time is not None:
404+
data["DailyUsage"] = {
405+
"kwhActual": float(day_usage.get("kwhActual") or 0),
406+
"billingCharge": float(
407+
day_usage.get("billingCharge") or 0
408+
),
409+
"readTime": read_time,
410+
"reading": float(day_usage.get("reading") or 0),
411+
"netDeliveredKwh": float(
412+
day_usage.get("netDeliveredKwh") or 0
413+
),
414+
"netDeliveredReading": float(
415+
day_usage.get("netDeliveredReading") or 0
416+
),
417+
}
372418

373419
except Exception as e:
374420
_LOGGER.error(e)

custom_components/fpl/sensor_ApplianceUsageSensor.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,27 @@ def __init__(self, coordinator, config, account):
2424
@property
2525
def native_value(self):
2626
appliance_usage = self.getData("appliance_usage")
27-
categories = appliance_usage.get("categories")
27+
if not appliance_usage:
28+
return self._attr_native_value
29+
categories = appliance_usage.get("categories") or []
2830
for category in categories:
2931
if category.get("category").lower() == self.CATEGORY_NAME.lower():
3032
self._attr_native_value = category.get("cost")
3133
return self._attr_native_value
3234

3335
def customAttributes(self):
3436
"""Return the state attributes."""
35-
# Add any extra attributes you want to expose here
3637
appliance_usage = self.getData("appliance_usage")
37-
attributes = {
38-
"startDate": datetime.strptime(
39-
appliance_usage.get("startDate"), "%Y-%m-%d"
40-
).strftime("%Y-%m-%d"),
41-
"endDate": datetime.strptime(
42-
appliance_usage.get("endDate"), "%Y-%m-%d"
43-
).strftime("%Y-%m-%d"),
38+
if not appliance_usage:
39+
return {}
40+
start_date = appliance_usage.get("startDate")
41+
end_date = appliance_usage.get("endDate")
42+
if not start_date or not end_date:
43+
return {}
44+
return {
45+
"startDate": datetime.strptime(start_date, "%Y-%m-%d").strftime("%Y-%m-%d"),
46+
"endDate": datetime.strptime(end_date, "%Y-%m-%d").strftime("%Y-%m-%d"),
4447
}
45-
return attributes
4648

4749

4850
class ApplianceUsageSensor(FplEnergyEntity):
@@ -63,25 +65,27 @@ def __init__(self, coordinator, config, account):
6365
@property
6466
def native_value(self):
6567
appliance_usage = self.getData("appliance_usage")
66-
categories = appliance_usage.get("categories")
68+
if not appliance_usage:
69+
return self._attr_native_value
70+
categories = appliance_usage.get("categories") or []
6771
for category in categories:
6872
if category.get("category").lower() == self.CATEGORY_NAME.lower():
6973
self._attr_native_value = category.get("kwh")
7074
return self._attr_native_value
7175

7276
def customAttributes(self):
7377
"""Return the state attributes."""
74-
# Add any extra attributes you want to expose here
7578
appliance_usage = self.getData("appliance_usage")
76-
attributes = {
77-
"startDate": datetime.strptime(
78-
appliance_usage.get("startDate"), "%Y-%m-%d"
79-
).strftime("%Y-%m-%d"),
80-
"endDate": datetime.strptime(
81-
appliance_usage.get("endDate"), "%Y-%m-%d"
82-
).strftime("%Y-%m-%d"),
79+
if not appliance_usage:
80+
return {}
81+
start_date = appliance_usage.get("startDate")
82+
end_date = appliance_usage.get("endDate")
83+
if not start_date or not end_date:
84+
return {}
85+
return {
86+
"startDate": datetime.strptime(start_date, "%Y-%m-%d").strftime("%Y-%m-%d"),
87+
"endDate": datetime.strptime(end_date, "%Y-%m-%d").strftime("%Y-%m-%d"),
8388
}
84-
return attributes
8589

8690

8791
class CoolingCostSensor(ApplianceCostSensor):

0 commit comments

Comments
 (0)