From 7cf8cbf219105fb83950473e4fa45c3587c02e87 Mon Sep 17 00:00:00 2001 From: Todd Medema Date: Fri, 12 Apr 2024 22:37:56 -0700 Subject: [PATCH] Factor lat/long into locations and sun calcs --- package-lock.json | 6 ++ package.json | 1 + src/Constants.tsx | 20 +++-- src/Types.tsx | 6 +- src/components/base/ChartSupplyDemand.tsx | 37 ++++++--- src/components/views/Facilities.tsx | 9 ++- src/components/views/LoadingContainer.tsx | 4 +- src/components/views/NewGame.tsx | 2 +- src/components/views/NewGameDetails.tsx | 4 +- src/data/Weather.tsx | 21 +++-- src/helpers/DateTime.tsx | 94 ++++++----------------- src/helpers/Energy.tsx | 26 +++++++ src/reducers/Game.tsx | 49 ++++++------ 13 files changed, 145 insertions(+), 134 deletions(-) create mode 100644 src/helpers/Energy.tsx diff --git a/package-lock.json b/package-lock.json index 2ed3718a..d6ce1613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "react-scripts": "5.0.1", "react-transition-group": "^4.4.5", "sass": "^1.72.0", + "suncalc": "^1.9.0", "typescript": "^4.9.5", "victory": "^37.0.1", "web-vitals": "^2.1.4" @@ -18157,6 +18158,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/suncalc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/suncalc/-/suncalc-1.9.0.tgz", + "integrity": "sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 18eeb13c..92e6b079 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-scripts": "5.0.1", "react-transition-group": "^4.4.5", "sass": "^1.72.0", + "suncalc": "^1.9.0", "typescript": "^4.9.5", "victory": "^37.0.1", "web-vitals": "^2.1.4" diff --git a/src/Constants.tsx b/src/Constants.tsx index 90e225bd..c2ad21b5 100644 --- a/src/Constants.tsx +++ b/src/Constants.tsx @@ -39,24 +39,32 @@ export const DIFFICULTIES = { }, } as { [index: string]: DifficultyMultipliersType }; -export const LOCATIONS = [ - { +export const LOCATIONS = { + PIT: { id: "PIT", name: "Pittsburgh, PA", + lat: 40.4406, + long: -79.9959, }, - { + SF: { id: "SF", name: "San Fransisco, CA", + lat: 37.7749, + long: -122.4194, }, - { + HNL: { id: "HNL", name: "Honolulu, HI", + lat: 21.3099, + long: -157.8581, }, - { + SJU: { id: "SJU", name: "San Juan, Puero Rico", + lat: 18.4671, + long: -66.1185, }, -] as LocationType[]; +} as { [id: string]: LocationType }; export const OUTSKIRTS_WIND_MULTIPLIER = 2; // https://github.com/toddmedema/electrify/issues/96 export const TICK_MS = { diff --git a/src/Types.tsx b/src/Types.tsx index 0d2666ff..6f8f9b19 100644 --- a/src/Types.tsx +++ b/src/Types.tsx @@ -21,10 +21,12 @@ export type MonthType = export type DifficultyType = "Intern" | "Employee" | "Manager" | "VP" | "CEO"; export type SpeedType = "PAUSED" | "SLOW" | "NORMAL" | "FAST"; -export type LocationIdType = "PIT" | "SF"; +export type LocationIdType = "PIT" | "SF" | "HNL" | "SJU"; export interface LocationType { id: LocationIdType; name: string; + lat: number; + long: number; } export type FuelNameType = "Coal" | "Wind" | "Sun" | "Natural Gas" | "Uranium"; @@ -88,8 +90,6 @@ export interface DateType { monthNumber: number; // 1 - 12 monthsEllapsed: number; year: number; - sunrise: number; - sunset: number; } export interface RawWeatherType { diff --git a/src/components/base/ChartSupplyDemand.tsx b/src/components/base/ChartSupplyDemand.tsx index 0e96eaaa..2d5cbb18 100644 --- a/src/components/base/ChartSupplyDemand.tsx +++ b/src/components/base/ChartSupplyDemand.tsx @@ -8,7 +8,7 @@ import { VictoryLine, VictoryTheme, } from "victory"; -import { getDateFromMinute } from "../../helpers/DateTime"; +import { getDateFromMinute, getSunriseSunset } from "../../helpers/DateTime"; import { formatWatts } from "../../helpers/Format"; import { getIntersectionX } from "../../helpers/Math"; import { @@ -17,6 +17,7 @@ import { demandColor, supplyColor, } from "../../Theme"; +import { LocationType } from "../../Types"; interface ChartData { minute: number; @@ -31,16 +32,17 @@ interface BlackoutEdges { export interface Props { currentMinute: number; + location: LocationType; height?: number; legend?: boolean; timeline: ChartData[]; startingYear: number; } -// TODO how to indicate reality vs forecast? Perhaps current time as a prop, and then split it in the chart +// TODO how to indicate history vs reality vs forecast? Perhaps current time as a prop, and then split it in the chart // and don't actually differentiate between reality + forecast in data? const ChartSupplyDemand = (props: Props): JSX.Element => { - const { startingYear, height, legend, timeline } = props; + const { startingYear, height, legend, timeline, location } = props; // Figure out the boundaries of the chart data let domainMin = 999999999999; let domainMax = 0; @@ -54,17 +56,30 @@ const ChartSupplyDemand = (props: Props): JSX.Element => { // Get sunrise and sunset, sliding forward if it's actually in the next day const date = getDateFromMinute(rangeMin, startingYear); const midnight = Math.floor(rangeMin / 1440) * 1440; - let sunrise = midnight + date.sunrise; - let sunset = midnight + date.sunset; + + let sunrise = + midnight + getSunriseSunset(date, location.lat, location.long).sunrise; + let sunset = + midnight + getSunriseSunset(date, location.lat, location.long).sunset; if (sunrise < rangeMin) { sunrise = midnight + 1440 + - getDateFromMinute(rangeMin + 1440, startingYear).sunrise; + getSunriseSunset( + getDateFromMinute(rangeMin + 1440, startingYear), + location.lat, + location.long + ).sunrise; } if (sunset < rangeMin) { sunset = - midnight + 1440 + getDateFromMinute(rangeMin + 1440, startingYear).sunset; + midnight + + 1440 + + getSunriseSunset( + getDateFromMinute(rangeMin + 1440, startingYear), + location.lat, + location.long + ).sunset; } // BLACKOUT CALCULATION @@ -95,7 +110,7 @@ const ChartSupplyDemand = (props: Props): JSX.Element => { prev.minute, prev.demandW, d.minute, - d.demandW, + d.demandW ); blackouts.push({ minute: intersectionTime, value: 0 }); blackouts.push({ minute: intersectionTime, value: domainMax }); @@ -111,7 +126,7 @@ const ChartSupplyDemand = (props: Props): JSX.Element => { prev.minute, prev.demandW, d.minute, - d.demandW, + d.demandW ); blackouts.push({ minute: intersectionTime, value: domainMax }); blackouts.push({ minute: intersectionTime, value: 0 }); @@ -128,10 +143,10 @@ const ChartSupplyDemand = (props: Props): JSX.Element => { // Divide between historic and forcast const currentMinute = props.currentMinute || 0; const historic = [...timeline].filter( - (d: ChartData) => d.minute <= currentMinute, + (d: ChartData) => d.minute <= currentMinute ); const forecast = [...timeline].filter( - (d: ChartData) => d.minute >= currentMinute, + (d: ChartData) => d.minute >= currentMinute ); const legendItems = [ diff --git a/src/components/views/Facilities.tsx b/src/components/views/Facilities.tsx index 7f031a44..e8d00c00 100644 --- a/src/components/views/Facilities.tsx +++ b/src/components/views/Facilities.tsx @@ -59,7 +59,7 @@ function FacilityListItem(props: FacilityListItemProps): JSX.Element { const percentBuilt = Math.round( ((facility.yearsToBuild - facility.yearsToBuildLeft) / facility.yearsToBuild) * - 100, + 100 ); secondaryText = `Building: ${percentBuilt}%, ${Math.ceil(props.facility.yearsToBuildLeft * 12)} months left`; } else if (facility.peakWh) { @@ -81,7 +81,7 @@ function FacilityListItem(props: FacilityListItemProps): JSX.Element { {...provided.dragHandleProps} style={getDraggableStyle( snapshot.isDragging, - provided.draggableProps.style, + provided.draggableProps.style )} > @@ -218,7 +218,7 @@ export default class Facilities extends React.Component { this.props.onReprioritize( result.source.index, - result.destination.index - result.source.index, + result.destination.index - result.source.index ); } @@ -233,6 +233,7 @@ export default class Facilities extends React.Component { height={180} timeline={game.timeline} currentMinute={game.date.minute} + location={game.location} legend={game.speed === "PAUSED"} startingYear={game.startingYear} /> @@ -273,7 +274,7 @@ export default class Facilities extends React.Component { spotInList={i} listLength={facilitiesCount} /> - ), + ) )} {provided.placeholder} diff --git a/src/components/views/LoadingContainer.tsx b/src/components/views/LoadingContainer.tsx index 750dc185..9beed7d8 100644 --- a/src/components/views/LoadingContainer.tsx +++ b/src/components/views/LoadingContainer.tsx @@ -26,7 +26,7 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => { if (!scenario) { return alert("Unknown scenario ID " + game.scenarioId); } - const location = LOCATIONS.find((s) => s.id === scenario.locationId); + const location = LOCATIONS[scenario.locationId]; if (!location) { return alert("Unknown location ID " + scenario.locationId); } @@ -43,7 +43,7 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => { cash: scenario.cash, customers: 1030000, location, - }), + }) ); dispatch(loaded()); diff --git a/src/components/views/NewGame.tsx b/src/components/views/NewGame.tsx index 129f5721..3d31394e 100644 --- a/src/components/views/NewGame.tsx +++ b/src/components/views/NewGame.tsx @@ -63,7 +63,7 @@ interface ScenarioListItemProps { function ScenarioListItem(props: ScenarioListItemProps): JSX.Element { const { s, onDetails } = props; - const location = LOCATIONS.find((l) => l.id === s.locationId) || { + const location = LOCATIONS[s.locationId] || { name: "UNKNOWN", }; const summary = diff --git a/src/components/views/NewGameDetails.tsx b/src/components/views/NewGameDetails.tsx index 5e45bb76..94b77c5c 100644 --- a/src/components/views/NewGameDetails.tsx +++ b/src/components/views/NewGameDetails.tsx @@ -62,9 +62,7 @@ export default class NewGameDetails extends React.Component { SCENARIOS.find((s) => s.id === props.game.scenarioId) || null; this.state = { scenario, - location: scenario - ? LOCATIONS.find((s) => s.id === scenario.locationId) || null - : null, + location: scenario ? LOCATIONS[scenario.locationId || null] : null, }; if (props.uid) { this.loadScores(props.uid); diff --git a/src/data/Weather.tsx b/src/data/Weather.tsx index e81e41c2..1382e98e 100644 --- a/src/data/Weather.tsx +++ b/src/data/Weather.tsx @@ -1,6 +1,7 @@ import { DAYS_PER_MONTH, DAYS_PER_YEAR } from "../Constants"; import { DateType, RawWeatherType } from "../Types"; import { getRandomRange } from "../helpers/Math"; +import { getSunriseSunset } from "../helpers/DateTime"; const Papa = require("papaparse"); @@ -39,7 +40,7 @@ export function initWeather(location: string, callback?: any) { complete() { if (weather.length !== EXPECTED_ROWS) { console.warn( - `Weather data for ${location} appears to be incomplete. Found ${weather.length} rows, expected ${EXPECTED_ROWS}`, + `Weather data for ${location} appears to be incomplete. Found ${weather.length} rows, expected ${EXPECTED_ROWS}` ); } if (callback) { @@ -78,15 +79,22 @@ export function getWeather(date: DateType): RawWeatherType { }; } -// 0-1, percent of sun's energy hitting a unit of land relative to max +// 0-1, percent of sun's energy hitting a unit of land relative to max at equator (~1360 w/m2) // Is later multiplied by cloudiness // TODO change to watts per sq meter or some fixed value, and verify that it's returning reasonably accurate values per location and season // (hoping that day length alone is a sufficient proxy / ideally don't need to make it any more complex) -export function getRawSunlightPercent(date: DateType) { - if (date.minuteOfDay >= date.sunrise && date.minuteOfDay <= date.sunset) { +// https://earthobservatory.nasa.gov/features/EnergyBalance/page2.php +// indicates a roughly linear correlation that each degree off from 0*N/S = 0.7% less sunlight +export function getRawSunlightPercent( + date: DateType, + lat: number, + long: number +) { + const { sunrise, sunset } = getSunriseSunset(date, lat, long); + if (date.minuteOfDay >= sunrise && date.minuteOfDay <= sunset) { const minutesFromDark = Math.min( - date.minuteOfDay - date.sunrise, - date.sunset - date.minuteOfDay, + date.minuteOfDay - sunrise, + sunset - date.minuteOfDay ); // TODO fix the pointiness, esp in shorter winter months // Maybe by factoring in day lenght to determine the shape of the curve? @@ -94,6 +102,7 @@ export function getRawSunlightPercent(date: DateType) { // Day length / minutes from dark used as proxy for season / max sun height // Rough approximation of solar output: https://www.wolframalpha.com/input/?i=plot+1%2F%281+%2B+e+%5E+%28-0.015+*+%28x+-+260%29%29%29+from+0+to+420 // Solar panels generally follow a Bell curve + // Potential more complex model for solar panels: https://pro.arcgis.com/en/pro-app/3.1/tool-reference/spatial-analyst/how-solar-radiation-is-calculated.htm return 1 / (1 + Math.pow(Math.E, -0.015 * (minutesFromDark - 260))); } return 0; diff --git a/src/helpers/DateTime.tsx b/src/helpers/DateTime.tsx index 3a40dbef..3ec7bd65 100644 --- a/src/helpers/DateTime.tsx +++ b/src/helpers/DateTime.tsx @@ -10,9 +10,9 @@ import { DateType, DerivedHistoryType, MonthlyHistoryType, - MonthType, TickPresentFutureType, } from "../Types"; +const SunCalc = require("suncalc"); export const EMPTY_HISTORY = { month: 0, @@ -34,7 +34,7 @@ export const EMPTY_HISTORY = { // edits acc in place to avoid making tons of extra objects export function reduceHistories( acc: MonthlyHistoryType, - t: MonthlyHistoryType, + t: MonthlyHistoryType ): MonthlyHistoryType { acc.supplyWh += t.supplyWh; acc.demandWh += t.demandWh; @@ -54,7 +54,7 @@ export function reduceHistories( } export function deriveExpandedSummary( - s: MonthlyHistoryType, + s: MonthlyHistoryType ): DerivedHistoryType { const expenses = s.expensesFuel + @@ -77,7 +77,7 @@ export function deriveExpandedSummary( export function summarizeTimeline( timeline: TickPresentFutureType[], startingYear: number, - filter?: (t: TickPresentFutureType) => boolean, + filter?: (t: TickPresentFutureType) => boolean ): MonthlyHistoryType { const summary = { ...EMPTY_HISTORY }; // Go in reverse so that the last values for ending values (like net worth are used) @@ -105,7 +105,7 @@ export function summarizeTimeline( export function summarizeHistory( timeline: MonthlyHistoryType[], - filter?: (t: MonthlyHistoryType) => boolean, + filter?: (t: MonthlyHistoryType) => boolean ): MonthlyHistoryType { const summary = { ...EMPTY_HISTORY }; // Go in reverse so that the last values for ending values (like net worth are used) @@ -119,7 +119,7 @@ export function summarizeHistory( export function getTimeFromTimeline( minute: number, - timeline: TickPresentFutureType[], + timeline: TickPresentFutureType[] ): null | TickPresentFutureType { if (!timeline[0]) { return null; @@ -149,73 +149,11 @@ export function formatMonthChartAxis(t: number, multiyear: boolean) { export function formatHour(date: DateType): string { const time = new Date( - `${date.year}-${date.monthNumber}-1 ${Math.floor(date.minuteOfDay / 60)}:00`, + `${date.year}-${date.monthNumber}-1 ${Math.floor(date.minuteOfDay / 60)}:00` ); return time.toLocaleString("en-US", { hour: "numeric", hour12: true }); } -// Based on SF/California for now, v2 take in / change by location -function getSunrise(month: MonthType) { - switch (month) { - case "Jan": - return 445; - case "Feb": - return 430; - case "Mar": - return 415; - case "Apr": - return 400; - case "May": - return 385; - case "Jun": - return 365; - case "Jul": - return 352; - case "Aug": - return 374; - case "Sep": - return 396; - case "Oct": - return 426; - case "Nov": - return 434; - case "Dec": - default: - return 440; - } -} - -// Based on SF/California for now, v2 change by location -function getSunset(month: MonthType) { - switch (month) { - case "Jan": - return 1020; - case "Feb": - return 1041; - case "Mar": - return 1062; - case "Apr": - return 1084; - case "May": - return 1134; - case "Jun": - return 1184; - case "Jul": - return 1235; - case "Aug": - return 1200; - case "Sep": - return 1164; - case "Oct": - return 1132; - case "Nov": - return 1095; - case "Dec": - default: - return 1055; - } -} - // Faster subset of getDateFromMinute export function getMonthYearFromMinute(minute: number, startingYear: number) { const dayOfGame = Math.floor(minute / 1440); @@ -230,9 +168,23 @@ export function getMonthYearFromMinute(minute: number, startingYear: number) { }; } +// returns minutes since midnight +export function getSunriseSunset(date: DateType, lat: number, long: number) { + const calc = SunCalc.getTimes( + new Date(`${date.month} 1, ${date.year}`), + lat, + long + ); + + return { + sunrise: calc.sunrise.getHours() * 60 + calc.sunrise.getMinutes(), + sunset: calc.sunset.getHours() * 60 + calc.sunset.getMinutes(), + }; +} + export function getDateFromMinute( minute: number, - startingYear: number, + startingYear: number ): DateType { const minuteOfDay = minute % 1440; const hourOfDay = Math.floor(minuteOfDay / 60); @@ -259,7 +211,5 @@ export function getDateFromMinute( monthNumber, monthsEllapsed, year, - sunrise: getSunrise(month), - sunset: getSunset(month), }; } diff --git a/src/helpers/Energy.tsx b/src/helpers/Energy.tsx new file mode 100644 index 00000000..a5bd7a21 --- /dev/null +++ b/src/helpers/Energy.tsx @@ -0,0 +1,26 @@ +import { OUTSKIRTS_WIND_MULTIPLIER } from "../Constants"; + +export function getWindOutputFactor(windKph: number) { + // Wind gradient, assuming 10m weather station, 100m wind turbine, neutral air above human habitation - https://en.wikipedia.org/wiki/Wind_gradient + // Divide by 5 to convert from kph to m/s + const turbineWindMS = + (OUTSKIRTS_WIND_MULTIPLIER * (windKph * Math.pow(100 / 10, 0.34))) / 5; + + // Production output is sloped from 3-14m/s, capped on zero and peak at both ends, and cut off >25m/s - http://www.wind-power-program.com/turbine_characteristics.htm + const windOutputFactor = + turbineWindMS < 3 || turbineWindMS > 25 + ? 0 + : Math.max(0, Math.min(1, (turbineWindMS - 3) / 11)); + + return windOutputFactor; +} + +// Sunlight percent is a proxy for time of year and lat/long +// Solar panels slightly less efficient in warm weather, declining about 1% efficiency per 1C starting at 10C +// TODO what about rain and snow, esp panels covered in snow? +export function getSolarOutputFactor( + sunlightPercent: number, + temepratureC: number +) { + return sunlightPercent * Math.max(1, 1 - (temepratureC - 10) / 100); +} diff --git a/src/reducers/Game.tsx b/src/reducers/Game.tsx index 47ce47cb..fae99b41 100644 --- a/src/reducers/Game.tsx +++ b/src/reducers/Game.tsx @@ -6,6 +6,7 @@ import { getTimeFromTimeline, summarizeHistory, summarizeTimeline, + getSunriseSunset, } from "../helpers/DateTime"; import { customersFromMarketingSpend, @@ -19,6 +20,7 @@ import { formatWattHours, } from "../helpers/Format"; import { arrayMove } from "../helpers/Math"; +import { getWindOutputFactor, getSolarOutputFactor } from "../helpers/Energy"; import { getFuelPricesPerMBTU } from "../data/FuelPrices"; import { getRawSunlightPercent, getWeather } from "../data/Weather"; import { dialogOpen, dialogClose, snackbarOpen } from "./UI"; @@ -40,6 +42,7 @@ import { TICKS_PER_YEAR, YEARS_PER_TICK, OUTSKIRTS_WIND_MULTIPLIER, + LOCATIONS, } from "../Constants"; import { GENERATORS, STORAGE } from "../Facilities"; import { logEvent } from "../Globals"; @@ -79,13 +82,10 @@ interface NewGameAction { } let previousSpeed = "PAUSED" as SpeedType; -let previousSunrise = 0; +let previousMonth = ""; const initialGame: GameType = { scenarioId: 0, - location: { - id: "SF", - name: "SF", - }, + location: LOCATIONS["SF"], difficulty: "Employee", speed: "PAUSED", inGame: false, @@ -128,9 +128,8 @@ export const gameSlice = createSlice({ if (now && prev) { updateSupplyFacilitiesFinances(state, prev, now); - if (previousSunrise !== state.date.sunrise) { - // If it's a new day / month - previousSunrise = state.date.sunrise; + if (previousMonth !== state.date.month) { + previousMonth = state.date.month; const history = state.monthlyHistory; const { cash, customers } = now; @@ -491,6 +490,11 @@ function getDemandW( prev.customers * (1 + ORGANIC_GROWTH_MAX_ANNUAL / TICKS_PER_YEAR) + marketingGrowth ); + const { sunrise, sunset } = getSunriseSunset( + date, + game.location.lat, + game.location.long + ); // https://www.eia.gov/todayinenergy/detail.php?id=830 // https://www.e-education.psu.edu/ebf200/node/151 @@ -499,8 +503,7 @@ function getDemandW( const temperatureNormalized = 0.0035 * Math.pow(now.temperatureC, 2) - 0.035 * now.temperatureC; const minutesFromDarkNormalized = - Math.min(date.minuteOfDay - date.sunrise, date.sunset - date.minuteOfDay) / - 420; + Math.min(date.minuteOfDay - sunrise, sunset - date.minuteOfDay) / 420; const minutesFromDarkLogistics = 1 / (1 + Math.pow(Math.E, -minutesFromDarkNormalized * 6)); const minutesFrom9amNormalized = Math.abs(date.minuteOfDay - 540) / 120; @@ -527,7 +530,9 @@ function reforecastWeatherAndPrices(state: GameType): TickPresentFutureType[] { return { ...t, ...fuelPrices, - sunlight: getRawSunlightPercent(date) * (weather.CLOUD_PCT / 100), + sunlight: + getRawSunlightPercent(date, state.location.lat, state.location.long) * + (weather.CLOUD_PCT / 100), windKph: OUTSKIRTS_WIND_MULTIPLIER * weather.WIND_KPH, temperatureC: weather.TEMP_C, }; @@ -572,21 +577,13 @@ function updateSupplyFacilitiesFinances( } }); - // Wind gradient, assuming 10m weather station, 100m wind turbine, neutral air above human habitation - https://en.wikipedia.org/wiki/Wind_gradient - // Divide by 5 to convert from kph to m/s - const turbineWindMS = - (OUTSKIRTS_WIND_MULTIPLIER * (now.windKph * Math.pow(100 / 10, 0.34))) / 5; - - // Production output is sloped from 3-14m/s, capped on zero and peak at both ends, and cut off >25m/s - http://www.wind-power-program.com/turbine_characteristics.htm - const windOutputFactor = - turbineWindMS < 3 || turbineWindMS > 25 - ? 0 - : Math.max(0, Math.min(1, (turbineWindMS - 3) / 11)); - - // Solar panels slightly less efficient in warm weather, declining about 1% efficiency per 1C starting at 10C - // TODO what about rain and snow, esp panels covered in snow? - const solarOutputFactor = - now.sunlight * Math.max(1, 1 - (now.temperatureC - 10) / 100); + const windOutputFactor = getWindOutputFactor(now.windKph); + const solarOutputFactor = getSolarOutputFactor( + now.sunlight, + now.temperatureC + ); + + console.log(now.minute, now.sunlight, solarOutputFactor); // Pre-check how much extra supply we'll need to charge batteries let indexOfLastUnchargedBattery = -1;