|
35 | 35 | from solisdash.config import get_settings |
36 | 36 | from solisdash.configfile import delete_toml, user_config_toml_path, write_toml |
37 | 37 | from solisdash.db import ensure_indexes |
38 | | -from solisdash.history import HistoryService, Series, parse_month |
| 38 | +from solisdash.history import ( |
| 39 | + METRIC_ENERGY, |
| 40 | + METRIC_POWER, |
| 41 | + METRIC_SUPPORTS, |
| 42 | + VIEW_ALL, |
| 43 | + VIEW_DAY, |
| 44 | + VIEW_MONTH, |
| 45 | + VIEW_YEAR, |
| 46 | + HistoryService, |
| 47 | + Series, |
| 48 | + metric_supports, |
| 49 | + parse_month, |
| 50 | +) |
39 | 51 | from solisdash.poller import Poller |
40 | 52 | from solisdash.ratelimit import TokenBucket |
41 | 53 | from solisdash.scheduler import build_scheduler |
@@ -778,70 +790,108 @@ async def history_poll_now( |
778 | 790 | ) |
779 | 791 |
|
780 | 792 |
|
| 793 | +_DAY_LABELS = { |
| 794 | + "power": ("Power", "kW"), |
| 795 | + "energy": ("Day energy", "kWh"), |
| 796 | + "battery": ("Battery SOC", "%"), |
| 797 | + "alarms": ("Open alarms", ""), |
| 798 | +} |
| 799 | +_DAILY_LABELS = { |
| 800 | + "energy": { |
| 801 | + "month": ("Daily energy", "kWh"), |
| 802 | + "year": ("Monthly energy", "kWh"), |
| 803 | + "all": ("Annual energy", "kWh"), |
| 804 | + }, |
| 805 | + "money": { |
| 806 | + "month": ("Daily revenue", ""), |
| 807 | + "year": ("Monthly revenue", ""), |
| 808 | + "all": ("Annual revenue", ""), |
| 809 | + }, |
| 810 | +} |
| 811 | + |
| 812 | + |
| 813 | +def _validate_metric(metric: str, view: str) -> None: |
| 814 | + if metric not in METRIC_SUPPORTS: |
| 815 | + raise HTTPException(status_code=400, detail=f"unknown metric: {metric!r}") |
| 816 | + if not metric_supports(metric, view): |
| 817 | + raise HTTPException( |
| 818 | + status_code=400, |
| 819 | + detail=f"metric {metric!r} is not available for the {view!r} view", |
| 820 | + ) |
| 821 | + |
| 822 | + |
781 | 823 | @app.get("/history/day.json") |
782 | 824 | async def history_day_json( |
783 | 825 | station_id: str | None = Query(None), |
784 | 826 | when: str = Query(..., description="YYYY-MM-DD"), |
| 827 | + metric: str = Query(METRIC_POWER), |
785 | 828 | history: HistoryService = Depends(get_history_service), |
786 | 829 | user: dict[str, Any] = Depends(require_user), |
787 | 830 | ) -> JSONResponse: |
| 831 | + _validate_metric(metric, VIEW_DAY) |
788 | 832 | sid = await _resolve_station_id(history, station_id) |
789 | 833 | if sid is None: |
790 | | - return JSONResponse(_empty_series_response("Power", "kW", station_id=None)) |
| 834 | + label, unit = _DAY_LABELS.get(metric, ("Series", "")) |
| 835 | + return JSONResponse(_empty_series_response(label, unit, station_id=None)) |
791 | 836 | try: |
792 | 837 | when_date = date.fromisoformat(when) |
793 | 838 | except ValueError as exc: |
794 | 839 | raise HTTPException(status_code=400, detail=str(exc)) from exc |
795 | | - series = await history.day_series(sid, when_date) |
| 840 | + series = await history.day_series(sid, when_date, metric=metric) |
796 | 841 | return JSONResponse({"station_id": sid, **series.to_json()}) |
797 | 842 |
|
798 | 843 |
|
799 | 844 | @app.get("/history/month.json") |
800 | 845 | async def history_month_json( |
801 | 846 | station_id: str | None = Query(None), |
802 | 847 | month: str = Query(..., description="YYYY-MM"), |
| 848 | + metric: str = Query(METRIC_ENERGY), |
803 | 849 | history: HistoryService = Depends(get_history_service), |
804 | 850 | user: dict[str, Any] = Depends(require_user), |
805 | 851 | ) -> JSONResponse: |
| 852 | + _validate_metric(metric, VIEW_MONTH) |
806 | 853 | sid = await _resolve_station_id(history, station_id) |
807 | 854 | if sid is None: |
808 | | - return JSONResponse(_empty_series_response("Daily energy", "kWh", station_id=None)) |
| 855 | + label, unit = _DAILY_LABELS[metric]["month"] |
| 856 | + return JSONResponse(_empty_series_response(label, unit, station_id=None)) |
809 | 857 | try: |
810 | 858 | parse_month(month) |
811 | 859 | except ValueError as exc: |
812 | 860 | raise HTTPException(status_code=400, detail=str(exc)) from exc |
813 | | - series = await history.month_daily(sid, month) |
| 861 | + series = await history.month_daily(sid, month, metric=metric) |
814 | 862 | return JSONResponse({"station_id": sid, **series.to_json()}) |
815 | 863 |
|
816 | 864 |
|
817 | 865 | @app.get("/history/year.json") |
818 | 866 | async def history_year_json( |
819 | 867 | station_id: str | None = Query(None), |
820 | 868 | year: int = Query(...), |
| 869 | + metric: str = Query(METRIC_ENERGY), |
821 | 870 | history: HistoryService = Depends(get_history_service), |
822 | 871 | user: dict[str, Any] = Depends(require_user), |
823 | 872 | ) -> JSONResponse: |
| 873 | + _validate_metric(metric, VIEW_YEAR) |
824 | 874 | sid = await _resolve_station_id(history, station_id) |
825 | 875 | if sid is None: |
826 | | - return JSONResponse( |
827 | | - _empty_series_response("Monthly energy", "kWh", station_id=None) |
828 | | - ) |
829 | | - series = await history.year_monthly(sid, year) |
| 876 | + label, unit = _DAILY_LABELS[metric]["year"] |
| 877 | + return JSONResponse(_empty_series_response(label, unit, station_id=None)) |
| 878 | + series = await history.year_monthly(sid, year, metric=metric) |
830 | 879 | return JSONResponse({"station_id": sid, **series.to_json()}) |
831 | 880 |
|
832 | 881 |
|
833 | 882 | @app.get("/history/all.json") |
834 | 883 | async def history_all_json( |
835 | 884 | station_id: str | None = Query(None), |
| 885 | + metric: str = Query(METRIC_ENERGY), |
836 | 886 | history: HistoryService = Depends(get_history_service), |
837 | 887 | user: dict[str, Any] = Depends(require_user), |
838 | 888 | ) -> JSONResponse: |
| 889 | + _validate_metric(metric, VIEW_ALL) |
839 | 890 | sid = await _resolve_station_id(history, station_id) |
840 | 891 | if sid is None: |
841 | | - return JSONResponse( |
842 | | - _empty_series_response("Annual energy", "kWh", station_id=None) |
843 | | - ) |
844 | | - series = await history.all_time(sid) |
| 892 | + label, unit = _DAILY_LABELS[metric]["all"] |
| 893 | + return JSONResponse(_empty_series_response(label, unit, station_id=None)) |
| 894 | + series = await history.all_time(sid, metric=metric) |
845 | 895 | return JSONResponse({"station_id": sid, **series.to_json()}) |
846 | 896 |
|
847 | 897 |
|
@@ -872,74 +922,82 @@ def _csv_response(body: str, filename: str) -> Response: |
872 | 922 | async def history_day_csv( |
873 | 923 | station_id: str | None = Query(None), |
874 | 924 | when: str = Query(..., description="YYYY-MM-DD"), |
| 925 | + metric: str = Query(METRIC_POWER), |
875 | 926 | history: HistoryService = Depends(get_history_service), |
876 | 927 | user: dict[str, Any] = Depends(require_user), |
877 | 928 | ) -> Response: |
| 929 | + _validate_metric(metric, VIEW_DAY) |
878 | 930 | sid = await _resolve_station_id(history, station_id) |
879 | 931 | if sid is None: |
880 | | - return _csv_response("timestamp_ms,power\n", "history-day.csv") |
| 932 | + return _csv_response(f"timestamp_ms,{metric}\n", f"history-day-{metric}.csv") |
881 | 933 | try: |
882 | 934 | when_date = date.fromisoformat(when) |
883 | 935 | except ValueError as exc: |
884 | 936 | raise HTTPException(status_code=400, detail=str(exc)) from exc |
885 | | - series = await history.day_series(sid, when_date) |
| 937 | + series = await history.day_series(sid, when_date, metric=metric) |
886 | 938 | return _csv_response( |
887 | | - _series_to_csv(series, x_header="timestamp_ms", y_header="power"), |
888 | | - f"history-day-{sid}-{when}.csv", |
| 939 | + _series_to_csv(series, x_header="timestamp_ms", y_header=metric), |
| 940 | + f"history-day-{metric}-{sid}-{when}.csv", |
889 | 941 | ) |
890 | 942 |
|
891 | 943 |
|
892 | 944 | @app.get("/history/month.csv") |
893 | 945 | async def history_month_csv( |
894 | 946 | station_id: str | None = Query(None), |
895 | 947 | month: str = Query(..., description="YYYY-MM"), |
| 948 | + metric: str = Query(METRIC_ENERGY), |
896 | 949 | history: HistoryService = Depends(get_history_service), |
897 | 950 | user: dict[str, Any] = Depends(require_user), |
898 | 951 | ) -> Response: |
| 952 | + _validate_metric(metric, VIEW_MONTH) |
899 | 953 | sid = await _resolve_station_id(history, station_id) |
900 | 954 | if sid is None: |
901 | | - return _csv_response("date,energy\n", "history-month.csv") |
| 955 | + return _csv_response(f"date,{metric}\n", f"history-month-{metric}.csv") |
902 | 956 | try: |
903 | 957 | parse_month(month) |
904 | 958 | except ValueError as exc: |
905 | 959 | raise HTTPException(status_code=400, detail=str(exc)) from exc |
906 | | - series = await history.month_daily(sid, month) |
| 960 | + series = await history.month_daily(sid, month, metric=metric) |
907 | 961 | return _csv_response( |
908 | | - _series_to_csv(series, x_header="date", y_header="energy"), |
909 | | - f"history-month-{sid}-{month}.csv", |
| 962 | + _series_to_csv(series, x_header="date", y_header=metric), |
| 963 | + f"history-month-{metric}-{sid}-{month}.csv", |
910 | 964 | ) |
911 | 965 |
|
912 | 966 |
|
913 | 967 | @app.get("/history/year.csv") |
914 | 968 | async def history_year_csv( |
915 | 969 | station_id: str | None = Query(None), |
916 | 970 | year: int = Query(...), |
| 971 | + metric: str = Query(METRIC_ENERGY), |
917 | 972 | history: HistoryService = Depends(get_history_service), |
918 | 973 | user: dict[str, Any] = Depends(require_user), |
919 | 974 | ) -> Response: |
| 975 | + _validate_metric(metric, VIEW_YEAR) |
920 | 976 | sid = await _resolve_station_id(history, station_id) |
921 | 977 | if sid is None: |
922 | | - return _csv_response("month,energy\n", "history-year.csv") |
923 | | - series = await history.year_monthly(sid, year) |
| 978 | + return _csv_response(f"month,{metric}\n", f"history-year-{metric}.csv") |
| 979 | + series = await history.year_monthly(sid, year, metric=metric) |
924 | 980 | return _csv_response( |
925 | | - _series_to_csv(series, x_header="month", y_header="energy"), |
926 | | - f"history-year-{sid}-{year}.csv", |
| 981 | + _series_to_csv(series, x_header="month", y_header=metric), |
| 982 | + f"history-year-{metric}-{sid}-{year}.csv", |
927 | 983 | ) |
928 | 984 |
|
929 | 985 |
|
930 | 986 | @app.get("/history/all.csv") |
931 | 987 | async def history_all_csv( |
932 | 988 | station_id: str | None = Query(None), |
| 989 | + metric: str = Query(METRIC_ENERGY), |
933 | 990 | history: HistoryService = Depends(get_history_service), |
934 | 991 | user: dict[str, Any] = Depends(require_user), |
935 | 992 | ) -> Response: |
| 993 | + _validate_metric(metric, VIEW_ALL) |
936 | 994 | sid = await _resolve_station_id(history, station_id) |
937 | 995 | if sid is None: |
938 | | - return _csv_response("year,energy\n", "history-all.csv") |
939 | | - series = await history.all_time(sid) |
| 996 | + return _csv_response(f"year,{metric}\n", f"history-all-{metric}.csv") |
| 997 | + series = await history.all_time(sid, metric=metric) |
940 | 998 | return _csv_response( |
941 | | - _series_to_csv(series, x_header="year", y_header="energy"), |
942 | | - f"history-all-{sid}.csv", |
| 999 | + _series_to_csv(series, x_header="year", y_header=metric), |
| 1000 | + f"history-all-{metric}-{sid}.csv", |
943 | 1001 | ) |
944 | 1002 |
|
945 | 1003 |
|
|
0 commit comments