From 8ff089828c337b0fb527ac5f715282a56034dd54 Mon Sep 17 00:00:00 2001 From: plocket <52798256+plocket@users.noreply.github.com> Date: Wed, 10 Apr 2024 00:58:19 +0000 Subject: [PATCH 1/2] Add file field to EnergyUseHistory Co-authored-by: Camden Blatchly Co-authored-by: hectorbenitez19 --- .../EnergyUseHistory.tsx | 35 +++++++++++++++++++ heat-stack/app/routes/_heat+/single.tsx | 1 + 2 files changed, 36 insertions(+) diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx index b378c532..55344b3c 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx @@ -20,6 +20,27 @@ export function EnergyUseHistory() {

Energy Use History

+ { + const file = event.target.files?.[0] + if (file) { + const reader = new FileReader() + reader.onloadend = () => { + //setPreviewImage(reader.result as string) + } + reader.readAsDataURL(file) + } + else { + setPreviewImage(null) + } + console.log('Boom!') + }} + name="energy_use_upload" + type="file" + accept="text/csv" + />
@@ -27,3 +48,17 @@ export function EnergyUseHistory() {
) } + + + +// const file = event.target.files?.[0] + +// if (file) { +// const reader = new FileReader() +// reader.onloadend = () => { +// setPreviewImage(reader.result as string) +// } +// reader.readAsDataURL(file) +// } else { +// setPreviewImage(null) +// } \ No newline at end of file diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 92b95a9d..587f06af 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -119,6 +119,7 @@ export default function Inputs() { method="post" onSubmit={form.onSubmit} action="/single" + encType="multipart/form-data" > From e54562b0a56660444b0affe3bcad7aef8ec3feef Mon Sep 17 00:00:00 2001 From: Thad Kerosky Date: Wed, 17 Apr 2024 03:44:12 +0000 Subject: [PATCH 2/2] fix typo on livingArea (wrongly livingSpace) to allow submitting, trial of Pyodide, commented Co-authored-by: Hector Benitez Co-authored-by: plocket Co-authored-by: Steve Breit Co-authored-by: thatoldplatitude --- .../EnergyUseHistory.tsx | 18 +- .../CaseSummaryComponents/HomeInformation.tsx | 6 +- heat-stack/app/routes/_heat+/Inputs1.tsx | 4 +- heat-stack/app/routes/_heat+/single.tsx | 98 +++++++++- heat-stack/app/utils/GeocodeUtil.js | 29 +++ heat-stack/app/utils/WeatherUtil.js | 31 ++++ heat-stack/app/utils/pyodide.util.js | 167 ++++++++++++++++++ heat-stack/package.json | 1 + 8 files changed, 339 insertions(+), 15 deletions(-) create mode 100644 heat-stack/app/utils/GeocodeUtil.js create mode 100644 heat-stack/app/utils/WeatherUtil.js create mode 100644 heat-stack/app/utils/pyodide.util.js diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx index 55344b3c..e89d1c5e 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx @@ -7,6 +7,7 @@ import { Button } from '#/app/components/ui/button.tsx' import { AnalysisHeader } from './AnalysisHeader.tsx' import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' +import { Upload } from 'lucide-react' // type EnergyUseProps = {fields: any}; @@ -26,14 +27,13 @@ export function EnergyUseHistory() { onChange={event => { const file = event.target.files?.[0] if (file) { - const reader = new FileReader() - reader.onloadend = () => { - //setPreviewImage(reader.result as string) - } - reader.readAsDataURL(file) - } - else { - setPreviewImage(null) + // const reader = new FileReader() + // reader.onloadend = async (event) => { + // console.log('reader.result', reader.result) + // } + // reader.readAsText(file) + } else { + // setPreviewImage(null) } console.log('Boom!') }} @@ -41,7 +41,7 @@ export function EnergyUseHistory() { type="file" accept="text/csv" /> - + diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx index 90f1744a..2cb35168 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx @@ -99,11 +99,11 @@ export function HomeInformation(props: HomeInformationProps) {
- +

diff --git a/heat-stack/app/routes/_heat+/Inputs1.tsx b/heat-stack/app/routes/_heat+/Inputs1.tsx index ab449dd8..e3a40908 100644 --- a/heat-stack/app/routes/_heat+/Inputs1.tsx +++ b/heat-stack/app/routes/_heat+/Inputs1.tsx @@ -23,7 +23,7 @@ const addressMaxLength = 100 const HomeInformationSchema = z.object({ name: z.string().min(1).max(nameMaxLength), address: z.string().min(1).max(addressMaxLength), - livingSpace: z.number().min(1), + livingArea: z.number().min(1), }) export async function action({ request, params }: ActionFunctionArgs) { @@ -56,7 +56,7 @@ export async function action({ request, params }: ActionFunctionArgs) { // - [ ] Build form #2 and #3 // - [ ] Form errors (if we think of a use case - 2 fields conflicting...) - const { name, address, livingSpace } = submission.value + const { name, address, livingArea } = submission.value // await updateNote({ id: params.noteId, title, content }) diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 587f06af..cf57c949 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -6,6 +6,9 @@ import { invariantResponse } from '@epic-web/invariant' import { json, ActionFunctionArgs } from '@remix-run/node' import { Form, redirect, useActionData } from '@remix-run/react' import { z } from 'zod' +import GeocodeUtil from "#app/utils/GeocodeUtil.js"; +import WeatherUtil from "#app/utils/WeatherUtil.js"; +import PyodideUtil from "#app/utils/pyodide.util.js"; // TODO NEXT WEEK // - [x] Server side error checking/handling @@ -32,6 +35,7 @@ import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/ import HeatLoadAnalysis from './heatloadanalysis.tsx' import { Button } from '#/app/components/ui/button.tsx' + const nameMaxLength = 50 const addressMaxLength = 100 @@ -69,12 +73,15 @@ export async function action({ request, params }: ActionFunctionArgs) { // Checks if url has a homeId parameter, throws 400 if not there // invariantResponse(params.homeId, 'homeId param is required') + console.log("action started") + const formData = await request.formData() const submission = parseWithZod(formData, { schema: Schema, }) if (submission.status !== 'success') { + console.error("submission failed",submission) return submission.reply() // submission.reply({ // // You can also pass additional error to the `reply` method @@ -97,10 +104,99 @@ export async function action({ request, params }: ActionFunctionArgs) { designTemperatureOverride } = submission.value // await updateNote({ id: params.noteId, title, content }) +//code snippet from - https://github.com/epicweb-dev/web-forms/blob/2c10993e4acffe3dd9ad7b9cb0cdf89ce8d46ecf/exercises/04.file-upload/01.solution.multi-part/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L180 + + // const formData = await parseMultipartFormData( + // request, + // createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + // ) + + console.log("loading PU/PM/GU/WU"); + + // CONSOLE: loading PU/PM/GU/WU + // Error: No known package with name 'pydantic_core' + // Error: No known package with name 'pydantic_core' + // at addPackageToLoad (/workspaces/home-energy-analysis-tool/heat-stack/public/pyodide-env/pyodide.asm.js:9:109097) + // at recursiveDependencies (/workspaces/home-energy-analysis-tool/heat-stack/public/pyodide-env/pyodide.asm.js:9:109370) + // at loadPackage (/workspaces/home-energy-analysis-tool/heat-stack/public/pyodide-env/pyodide.asm.js:9:111435) + // at initializePackageIndex (/workspaces/home-energy-analysis-tool/heat-stack/public/pyodide-env/pyodide.asm.js:9:108508) + + // const PU = PyodideUtil.getInstance(); + // const PM = await PU.getPyodideModule(); + const GU = new GeocodeUtil(); + const WU = new WeatherUtil(); + // console.log("loaded PU/PM/GU/WU"); + +/** + * + * @param longitude + * @param latitude + * @param start_date + * @param end_date + * @returns {SI,TIWD,BI} Summary input: hardcoded data.TIWD: TemperatureInput: WeatherData from calling open meto API + * Billing input: hardcoded data + * + * Function just to generate test data. inputs come from the values entered in from HomeInformation component + */ +async function genny(longitude: number, latitude: number, start_date: string, end_date: string) { + // SI = new SummaryInput(6666,"GAS",80,67,null,null,60); + // was living_area: number, fuel_type: FuelType, heating_system_efficiency: number, thermostat_set_point: number, setback_temperature: number | null, setback_hours_per_day: number | null, design_temperature: number + + type SchemaZodFromFormType = z.infer; + + + + const oldSummaryInput = { + living_area: 6666, + fuel_type: "GAS", + heating_system_efficiency: 80, + thermostat_set_point: 67, + setback_temperature: null, + setback_hours_per_day: null, + design_temperature: 60, + }; + + const SI: SchemaZodFromFormType = Schema.parse({ + livingArea: oldSummaryInput.living_area, + address: '123 Main St', // Provide a valid address + name: 'My Home', // Provide a valid name + fuelType: oldSummaryInput.fuel_type === 'GAS' ? 'Natural Gas' : oldSummaryInput.fuel_type, + heatingSystemEfficiency: oldSummaryInput.heating_system_efficiency, + thermostatSetPoint: oldSummaryInput.thermostat_set_point, + setbackTemperature: oldSummaryInput.setback_temperature, + setbackHoursPerDay: oldSummaryInput.setback_hours_per_day, + designTemperatureOverride: oldSummaryInput.design_temperature, + }); + + console.log("SI", SI) + + + // const TIWD: TemperatureInput = await WU.getThatWeathaData(longitude, latitude, start_date, end_date); + const TIWD = await WU.getThatWeathaData(longitude, latitude, start_date, end_date); + const BI = [{ + period_start_date: new Date("2023-12-30"),//new Date("2023-12-30"), + period_end_date: new Date("2024-01-06"), + usage:100, + inclusion_override: null + }]; + return {SI, TIWD, BI}; +} + + + let { x, y } = await GU.getLL(address); + console.log("geocoded", x,y) - return redirect(`/inputs1`) + let { SI, TIWD, BI } = await genny(x,y,"2024-01-01","2024-01-03") + + // PU.runit(SI,null,TIWD,JSON.stringify(BI)); + // CSV entrypoint parse_gas_bill(data: str, company: NaturalGasCompany) + // Main form entrypoint + + return redirect(`/single`) } + + export default function Inputs() { const lastResult = useActionData() const [form, fields] = useForm({ diff --git a/heat-stack/app/utils/GeocodeUtil.js b/heat-stack/app/utils/GeocodeUtil.js new file mode 100644 index 00000000..a7103f84 --- /dev/null +++ b/heat-stack/app/utils/GeocodeUtil.js @@ -0,0 +1,29 @@ +const BASE_URL = "https://geocoding.geo.census.gov"; +const ADDRESS_ENDPOINT = "/geocoder/locations/address"; +const params = new URLSearchParams(); + +class GeocodeUtil { + + /** + * + * @param {*} street + * @param {*} city + * @param {*} state + * @returns x,y {x,y} lon/lat. If the given address was valid. I've implemented 0 handling here. + * This is the happiest of paths, with hardcoded values also... + */ + async getLL(address) { + params.append("onelineaddress",address); + params.append("format","json"); + params.append("benchmark",2020); + + let url = new URL(BASE_URL+ADDRESS_ENDPOINT+"?"+params.toString()); + let rezzy = await fetch(url); + let jrez = await rezzy.json(); + let coordz = jrez.result.addressMatches[0].coordinates; + console.log(coordz); + return coordz; + } +} + +export default GeocodeUtil; \ No newline at end of file diff --git a/heat-stack/app/utils/WeatherUtil.js b/heat-stack/app/utils/WeatherUtil.js new file mode 100644 index 00000000..3ab4364b --- /dev/null +++ b/heat-stack/app/utils/WeatherUtil.js @@ -0,0 +1,31 @@ +import { time } from "console"; + +const BASE_URL = "https://archive-api.open-meteo.com"; +const WHATEVER_PATH = "/v1/archive"; +const params = new URLSearchParams(); + +class WeatherUtil { + + async getThatWeathaData(longitude,latitude,startDate, endDate) { + params.append("latitude",latitude); + params.append("longitude",longitude); + params.append("daily","temperature_2m_max"); + params.append("timezone","America/New_York"); + params.append("start_date",startDate); + params.append("end_date",endDate); + params.append("temperature_unit","fahrenheit"); + + let url = new URL(BASE_URL+WHATEVER_PATH+"?"+params.toString()); + let rezzy = await fetch(url); + let jrez = await rezzy.json(); + let dates = []; + jrez.daily.time.forEach(timeStr => { + dates.push(new Date(timeStr)); + }); + let temperatures = jrez.daily.temperature_2m_max + // console.log({dates,temperatures}); + return {dates,temperatures}; + } +} + +export default WeatherUtil; \ No newline at end of file diff --git a/heat-stack/app/utils/pyodide.util.js b/heat-stack/app/utils/pyodide.util.js new file mode 100644 index 00000000..c43b1edd --- /dev/null +++ b/heat-stack/app/utils/pyodide.util.js @@ -0,0 +1,167 @@ +import * as pyodideModule from 'pyodide' + + +class PyodideUtil { + static _instance; + constructor() { + // if(PyodideUtil._instance) { + // return PyodideUtil._instance; + // } + // PyodideUtil._instance = this; + // return this; + } + + static getInstance() { + if(!this._instance) { + this._instance = new PyodideUtil(); + } + return this._instance; + + } + + async getPyodideModule() { + if(!this.pyodideModule) { + this.pyodideModule = await loadPyodideModule(); + } + return this.pyodideModule; + } + async getEngineModule() { + if(!this.pyodideModule) { + throw new Error("Util not created."); + } + return await getEngine(this.pyodideModule); + } + + makePyImpObj(EngineObj) { + let name = EngineObj.constructor.name; + let r = this.pyodideModule.runPython(` + from rules_engine.pydantic_models import ${name} + from pyodide.ffi import to_js + import js + ${name}`); + return r; + + // return await this.pyodideModule.toPy(thing); + } + + async makeSI(si) { + console.log("Makin SI name:"+ si.name); + let f = this.pyodideModule.runPython(` + def f(s): + return SummaryInput(**s) + f + `); + let fr = f(si); + console.log(`FR: ${fr}`); + return fr; + } + + getSIO() { + let r = this.pyodideModule.runPython(` + from rules_engine.pydantic_models import SummaryInput + from pyodide.ffi import to_js + import js + SummaryInput`); + return r; + } + + runit(rs,rn,rt,rb) { + let pyProcess = this.pyodideModule.runPython(` + import json + from pyodide.ffi import to_js, JsProxy + from pyodide.code import run_js + import js + from datetime import date + from rules_engine import engine + from rules_engine.pydantic_models import ( + AnalysisType, + BalancePointGraph, + BalancePointGraphRow, + Constants, + DhwInput, + FuelType, + NaturalGasBillingInput, + NormalizedBillingPeriodRecordInput, + OilPropaneBillingInput, + SummaryInput, + SummaryOutput, + TemperatureInput, + ) + + def default_converter(value,_i1m,_i2): + #print("value: "+str(value)) + if 'Date' == str(value.constructor.name): + return date.fromtimestamp(value.valueOf() / 1000) + return value + + def process(s,n,t,b): + print("S INIT: ") + print(type(s)) + + s = s.as_object_map() + sv = s.values() + svm = sv._mapping + + + t = t.to_py(default_converter=default_converter) + + summa_inpoot = SummaryInput(**svm) + tempinz = TemperatureInput(**t) + + + b = json.loads(b) + billy_normz = [] + for x in b: + print(x) + billy_normz.append(NormalizedBillingPeriodRecordInput(**x)) + + + print(type(summa_inpoot)) + print(type(tempinz)) + print(type(billy_normz)) + print(summa_inpoot) + print(tempinz) + print(billy_normz) + + r = engine.get_outputs_normalized(summa_inpoot,None,tempinz,billy_normz) + print(r) + return r + process + `); + + let rezz = pyProcess(rs,rn,rt,rb); + console.log(`Le Rezz: ${rezz}`); + } +} + + + +const loadPyodideModule = async () => { + // public folder: + return await pyodideModule.loadPyodide({ + indexURL: 'public/pyodide-env/', + packages:["numpy","pydantic","pydantic_core","annotated_types","rules_engine"] + }); +} + +const getEngine = async(pyodide) => { + return await pyodide.pyimport("rules_engine.engine"); +} + +const testPythonScript = async () => { + const pyodide = await loadPyodideModule() + + let pm = await pyodide.pyimport("rules_engine.engine"); + let r = pm.hdd(57,60); + console.log(pm.toString()); + console.log(r); + // window.pydd = pyodide; //no eye deer what this does. hopefully its not needed after moving. + + return pyodide; +} + + +const getPydanticModel = () => { + +} +export default PyodideUtil; \ No newline at end of file diff --git a/heat-stack/package.json b/heat-stack/package.json index e9f06966..1b0a225c 100644 --- a/heat-stack/package.json +++ b/heat-stack/package.json @@ -21,6 +21,7 @@ "start": "cross-env NODE_ENV=production node .", "start:mocks": "cross-env NODE_ENV=production MOCKS=true tsx .", "test": "cd ../rules-engine && python3 -m venv venv && . venv/bin/activate && pip install -q build && python3 -m build && cd ../heat-stack && vitest && rm -rf ../rules-engine/dist ", + "buildpy": "cd ../rules-engine && python3 -m venv venv && . venv/bin/activate && pip install -q build && python3 -m build && cp dist/rules_engine-0.0.1-py3-none-any.whl ../heat-stack/public/pyodide-env", "coverage": "vitest run --coverage", "test:e2e": "npm run test:e2e:dev --silent", "test:e2e:dev": "playwright test --ui",