From 0a16459ea5b9d98481607275a742a0cc10963ba8 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:51:52 -0400 Subject: [PATCH 01/13] Wind speed calculation & tests --- __tests__/repositories/gfs.test.js | 1261 ++++++++++++++-------------- __tests__/services/metrics.test.js | 43 +- __tests__/services/rules.test.js | 4 + src/repositories/gfs.js | 22 +- src/services/metrics.js | 12 +- 5 files changed, 710 insertions(+), 632 deletions(-) diff --git a/__tests__/repositories/gfs.test.js b/__tests__/repositories/gfs.test.js index aec4c5b..155a674 100644 --- a/__tests__/repositories/gfs.test.js +++ b/__tests__/repositories/gfs.test.js @@ -3,637 +3,652 @@ const noaa_gfs = require("noaa-gfs-js"); jest.mock("noaa-gfs-js"); describe("Obtain NOAA GFS data", () => { - test("Fetch a metric", async () => { - noaa_gfs.get_gfs_data.mockImplementation((precision, date, hour, latRange, lonRange, samples, metric, format) => { - return Promise.resolve(mockAnyData); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("Fetch a metric", async () => { + noaa_gfs.get_gfs_data.mockImplementation((precision, date, hour, latRange, lonRange, samples, metric, format) => { + return Promise.resolve(mockAnyData); + }); - const results = await gfsRepository.getMetric(47.6205099, -122.3518523, 'my_metric'); - // We expect there are going to be count + 1 samples, with the last being ultimately discarded - expect(results.array_format).toHaveLength((gfsRepository.sampleCount + 1) * 4); + const results = await gfsRepository.getMetric(47.6205099, -122.3518523, 'my_metric'); + // We expect there are going to be count + 1 samples, with the last being ultimately discarded + expect(results.array_format).toHaveLength((gfsRepository.sampleCount + 1) * 4); + }); + + test("Aggregate a metric", async () => { + noaa_gfs.get_gfs_data.mockImplementation((precision, date, hour, latRange, lonRange, samples, metric, format) => { + return Promise.resolve(mockAnyData); }); - test("Aggregate a metric", async () => { - noaa_gfs.get_gfs_data.mockImplementation((precision, date, hour, latRange, lonRange, samples, metric, format) => { - return Promise.resolve(mockAnyData); - }); + const results = await gfsRepository.getAggregateMetric(47.6205099, -122.3518523, 'my_metric'); + expect(results).toHaveLength(gfsRepository.sampleCount); + expect(results[10].value).toBe(0.000030000001); + }); + + test("Calculate wind speed", async () => { + noaa_gfs.get_gfs_data.mockImplementation((precision, date, hour, latRange, lonRange, samples, metric, format) => { + return Promise.resolve(mockAnyData); + }); - const results = await gfsRepository.getAggregateMetric(47.6205099, -122.3518523, 'my_metric'); - expect(results).toHaveLength(gfsRepository.sampleCount); - expect(results[10].value).toBe(0.000030000001); + const results = await gfsRepository.getWindSpeed(47.6205099, -122.3518523); + console.log(results); + expect(results).toHaveLength(gfsRepository.sampleCount); + expect(results[10].value).toBe(Math.sqrt(Math.pow(0.000030000001, 2) + Math.pow(0.000030000001, 2))); }); }); const mockAnyData = { - "array_format": [ - { - "time": "8/19/2024, 12:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 12:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 12:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 12:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 3:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 3:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 3:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 3:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 6:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 6:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 6:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 6:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 9:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 9:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/19/2024, 9:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/19/2024, 9:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 6:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 6:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 6:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 6:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 9:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 9:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 9:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 9:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 12:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/20/2024, 3:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0.0000016 - }, - { - "time": "8/20/2024, 3:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0.000004 - }, - { - "time": "8/20/2024, 6:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0.0000064 - }, - { - "time": "8/20/2024, 6:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0.000030000001 - }, - { - "time": "8/20/2024, 6:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0.0000152 - }, - { - "time": "8/20/2024, 6:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0.0000852 - }, - { - "time": "8/20/2024, 9:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0.000026400001 - }, - { - "time": "8/20/2024, 9:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0.0000128 - }, - { - "time": "8/20/2024, 9:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 8e-7 - }, - { - "time": "8/20/2024, 9:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/21/2024, 12:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0.00013680001 - }, - { - "time": "8/21/2024, 12:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0.0000744 - }, - { - "time": "8/21/2024, 12:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 12:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/21/2024, 3:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0.0000496 - }, - { - "time": "8/21/2024, 3:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0.0000504 - }, - { - "time": "8/21/2024, 3:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 3:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/21/2024, 6:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 6:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/21/2024, 6:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 6:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/21/2024, 9:00:00 AM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 9:00:00 AM", - "lat": 47.5, - "lon": -122.25, - "value": 0.0000024 - }, - { - "time": "8/21/2024, 9:00:00 AM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 9:00:00 AM", - "lat": 47.75, - "lon": -122.25, - "value": 0 - }, - { - "time": "8/21/2024, 12:00:00 PM", - "lat": 47.5, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 12:00:00 PM", - "lat": 47.5, - "lon": -122.25, - "value": 0.0000024 - }, - { - "time": "8/21/2024, 12:00:00 PM", - "lat": 47.75, - "lon": -122.5, - "value": 0 - }, - { - "time": "8/21/2024, 12:00:00 PM", - "lat": 47.75, - "lon": -122.25, - "value": 0 + "array_format": [ + { + "time": "8/19/2024, 12:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 12:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 12:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 12:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 3:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 3:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 3:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 3:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 6:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 6:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 6:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 6:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 9:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 9:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/19/2024, 9:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/19/2024, 9:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 6:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 6:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 6:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 6:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 9:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 9:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 9:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 9:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 12:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/20/2024, 3:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0.0000016 + }, + { + "time": "8/20/2024, 3:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0.000004 + }, + { + "time": "8/20/2024, 6:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0.0000064 + }, + { + "time": "8/20/2024, 6:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0.000030000001 + }, + { + "time": "8/20/2024, 6:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0.0000152 + }, + { + "time": "8/20/2024, 6:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0.0000852 + }, + { + "time": "8/20/2024, 9:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0.000026400001 + }, + { + "time": "8/20/2024, 9:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0.0000128 + }, + { + "time": "8/20/2024, 9:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 8e-7 + }, + { + "time": "8/20/2024, 9:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/21/2024, 12:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0.00013680001 + }, + { + "time": "8/21/2024, 12:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0.0000744 + }, + { + "time": "8/21/2024, 12:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 12:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/21/2024, 3:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0.0000496 + }, + { + "time": "8/21/2024, 3:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0.0000504 + }, + { + "time": "8/21/2024, 3:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 3:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/21/2024, 6:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 6:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/21/2024, 6:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 6:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/21/2024, 9:00:00 AM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 9:00:00 AM", + "lat": 47.5, + "lon": -122.25, + "value": 0.0000024 + }, + { + "time": "8/21/2024, 9:00:00 AM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 9:00:00 AM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + }, + { + "time": "8/21/2024, 12:00:00 PM", + "lat": 47.5, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 12:00:00 PM", + "lat": 47.5, + "lon": -122.25, + "value": 0.0000024 + }, + { + "time": "8/21/2024, 12:00:00 PM", + "lat": 47.75, + "lon": -122.5, + "value": 0 + }, + { + "time": "8/21/2024, 12:00:00 PM", + "lat": 47.75, + "lon": -122.25, + "value": 0 + } + ], + "obj_format": { + "8/19/2024, 12:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 } - ], - "obj_format": { - "8/19/2024, 12:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/19/2024, 3:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/19/2024, 6:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/19/2024, 9:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/20/2024, 12:00:00 AM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/20/2024, 3:00:00 AM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/20/2024, 6:00:00 AM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/20/2024, 9:00:00 AM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/20/2024, 12:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/20/2024, 3:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0.0000016, - "-122.25": 0.000004 - } - }, - "8/20/2024, 6:00:00 PM": { - "47.5": { - "-122.5": 0.0000064, - "-122.25": 0.000030000001 - }, - "47.75": { - "-122.5": 0.0000152, - "-122.25": 0.0000852 - } - }, - "8/20/2024, 9:00:00 PM": { - "47.5": { - "-122.5": 0.000026400001, - "-122.25": 0.0000128 - }, - "47.75": { - "-122.5": 8e-7, - "-122.25": 0 - } - }, - "8/21/2024, 12:00:00 AM": { - "47.5": { - "-122.5": 0.00013680001, - "-122.25": 0.0000744 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/21/2024, 3:00:00 AM": { - "47.5": { - "-122.5": 0.0000496, - "-122.25": 0.0000504 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/21/2024, 6:00:00 AM": { - "47.5": { - "-122.5": 0, - "-122.25": 0 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/21/2024, 9:00:00 AM": { - "47.5": { - "-122.5": 0, - "-122.25": 0.0000024 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } - }, - "8/21/2024, 12:00:00 PM": { - "47.5": { - "-122.5": 0, - "-122.25": 0.0000024 - }, - "47.75": { - "-122.5": 0, - "-122.25": 0 - } + }, + "8/19/2024, 3:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/19/2024, 6:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/19/2024, 9:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/20/2024, 12:00:00 AM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 } }, - "times": [ - "8/19/2024, 12:00:00 PM", - "8/19/2024, 3:00:00 PM", - "8/19/2024, 6:00:00 PM", - "8/19/2024, 9:00:00 PM", - "8/20/2024, 12:00:00 AM", - "8/20/2024, 3:00:00 AM", - "8/20/2024, 6:00:00 AM", - "8/20/2024, 9:00:00 AM", - "8/20/2024, 12:00:00 PM", - "8/20/2024, 3:00:00 PM", - "8/20/2024, 6:00:00 PM", - "8/20/2024, 9:00:00 PM", - "8/21/2024, 12:00:00 AM", - "8/21/2024, 3:00:00 AM", - "8/21/2024, 6:00:00 AM", - "8/21/2024, 9:00:00 AM", - "8/21/2024, 12:00:00 PM" - ], - "lats": [ - 47.5, - 47.75 - ], - "lons": [ - -122.5, - -122.25 - ], - "levs": [], - "url": "https://nomads.ncep.noaa.gov/dods/gfs_0p25/gfs20240819/gfs_0p25_12z.ascii?pratesfc[0:17][550:551][950:951]" - }; \ No newline at end of file + "8/20/2024, 3:00:00 AM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/20/2024, 6:00:00 AM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/20/2024, 9:00:00 AM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/20/2024, 12:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/20/2024, 3:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0.0000016, + "-122.25": 0.000004 + } + }, + "8/20/2024, 6:00:00 PM": { + "47.5": { + "-122.5": 0.0000064, + "-122.25": 0.000030000001 + }, + "47.75": { + "-122.5": 0.0000152, + "-122.25": 0.0000852 + } + }, + "8/20/2024, 9:00:00 PM": { + "47.5": { + "-122.5": 0.000026400001, + "-122.25": 0.0000128 + }, + "47.75": { + "-122.5": 8e-7, + "-122.25": 0 + } + }, + "8/21/2024, 12:00:00 AM": { + "47.5": { + "-122.5": 0.00013680001, + "-122.25": 0.0000744 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/21/2024, 3:00:00 AM": { + "47.5": { + "-122.5": 0.0000496, + "-122.25": 0.0000504 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/21/2024, 6:00:00 AM": { + "47.5": { + "-122.5": 0, + "-122.25": 0 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/21/2024, 9:00:00 AM": { + "47.5": { + "-122.5": 0, + "-122.25": 0.0000024 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + }, + "8/21/2024, 12:00:00 PM": { + "47.5": { + "-122.5": 0, + "-122.25": 0.0000024 + }, + "47.75": { + "-122.5": 0, + "-122.25": 0 + } + } + }, + "times": [ + "8/19/2024, 12:00:00 PM", + "8/19/2024, 3:00:00 PM", + "8/19/2024, 6:00:00 PM", + "8/19/2024, 9:00:00 PM", + "8/20/2024, 12:00:00 AM", + "8/20/2024, 3:00:00 AM", + "8/20/2024, 6:00:00 AM", + "8/20/2024, 9:00:00 AM", + "8/20/2024, 12:00:00 PM", + "8/20/2024, 3:00:00 PM", + "8/20/2024, 6:00:00 PM", + "8/20/2024, 9:00:00 PM", + "8/21/2024, 12:00:00 AM", + "8/21/2024, 3:00:00 AM", + "8/21/2024, 6:00:00 AM", + "8/21/2024, 9:00:00 AM", + "8/21/2024, 12:00:00 PM" + ], + "lats": [ + 47.5, + 47.75 + ], + "lons": [ + -122.5, + -122.25 + ], + "levs": [], + "url": "https://nomads.ncep.noaa.gov/dods/gfs_0p25/gfs20240819/gfs_0p25_12z.ascii?pratesfc[0:17][550:551][950:951]" +}; \ No newline at end of file diff --git a/__tests__/services/metrics.test.js b/__tests__/services/metrics.test.js index fbe01f7..e91a8cd 100644 --- a/__tests__/services/metrics.test.js +++ b/__tests__/services/metrics.test.js @@ -4,8 +4,13 @@ jest.mock("../../src/repositories/gfs.js"); const configRepository = require("../../src/repositories/config.js"); jest.mock("../../src/repositories/config.js"); -gfsRepository.getPrecipitationRate.mockImplementation((lat, lon, metric) => Promise.resolve(mockPrecipitationRateData)); -gfsRepository.getPrecipitableWater.mockImplementation((lat, lon, metric) => Promise.resolve(mockPrecipitableWaterData)); +gfsRepository.getPrecipitationRate.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); +gfsRepository.getPrecipitableWater.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); +gfsRepository.getCloudWater.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); +gfsRepository.getRelativeHumidity.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); +gfsRepository.getGroundTemperature.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); +gfsRepository.getWindSpeed.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); + configRepository.get.mockImplementation((key) => { switch (key) { case 'latitude': return 47.6205099; @@ -14,6 +19,10 @@ configRepository.get.mockImplementation((key) => { }); describe("Get forecast metrics", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test("Precipitation rate", async () => { jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); const result = await metricsService.fetch(); @@ -26,9 +35,35 @@ describe("Get forecast metrics", () => { const result = await metricsService.fetch(); expect(result.maxPrecipitable).toBe(45.5); }); + + test("Cloud water", async () => { + jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); + const result = await metricsService.fetch(); + expect(result.maxCloudWater).toBe(0.00010161692339361316); + }); + + test("Humidity", async () => { + jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); + const result = await metricsService.fetch(); + expect(result.priorRelativeHumidity).toBe(0); + expect(result.forecastRelativeHumidity).toBe(0); + }); + + test("Ground temperature", async () => { + jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); + const result = await metricsService.fetch(); + expect(result.priorGroundTemp).toBe(0); + expect(result.forecastGroundTemp).toBe(45.5); + }); + + test("Wind speed", async () => { + jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); + const result = await metricsService.fetch(); + expect(result.windSpeed).toBe(0.00003124313318054331); + }); }); -const mockPrecipitationRateData = [ +const mockDecimalData = [ { "time": new Date("2024-08-19T16:00:00.000Z"), "latitude": 47.75, @@ -143,7 +178,7 @@ const mockPrecipitationRateData = [ } ]; -const mockPrecipitableWaterData = [ +const mockNumericData = [ { "time": new Date("2024-08-19T16:00:00.000Z"), "latitude": 47.75, diff --git a/__tests__/services/rules.test.js b/__tests__/services/rules.test.js index 5e55c3f..9e0d5dd 100644 --- a/__tests__/services/rules.test.js +++ b/__tests__/services/rules.test.js @@ -12,6 +12,10 @@ configRepository.get.mockImplementation((key, fallback) => { const rulesService = require("../../src/services/rules.js"); describe("Precipitation rate", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test("Too much rain yesterday", async () => { const result = await rulesService.evaluate({ priorAccumulation: 10.5, diff --git a/src/repositories/gfs.js b/src/repositories/gfs.js index fa687a0..d8519e0 100644 --- a/src/repositories/gfs.js +++ b/src/repositories/gfs.js @@ -21,11 +21,29 @@ class GfsRepository { } async getRelativeHumidity(lat, lon) { - return await this.getAggregateMetric(lat, lon, 'rh30_0mb'); + return await this.getAggregateMetric(lat, lon, 'rhprs'); } async getGroundTemperature(lat, lon) { - return await this.getAggregateMetric(lat, lon, 'tmp30_0mb'); + return await this.getAggregateMetric(lat, lon, 'tmpprs'); + } + + async getWindSpeed(lat, lon) { + const windU = await this.getAggregateMetric(lat, lon, 'ugrdprs'); + const windV = await this.getAggregateMetric(lat, lon, 'vgrdprs'); + + const windUByTime = windU.reduce((acc, result) => acc.set(result.time.toISOString(), result), new Map()); + const windSpeed = windV.map((resultV) => { + const resultU = windUByTime.get(resultV.time.toISOString()); + return { + ...resultV, + uValue: resultU.value, + vValue: resultV.value, + value: Math.sqrt((resultU.value * resultU.value) + (resultV.value * resultV.value)) + }; + }); + + return Array.from(windSpeed.values()); } async getAggregateMetric(lat, lon, metric) { diff --git a/src/services/metrics.js b/src/services/metrics.js index 131caf1..915a5ad 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -23,12 +23,17 @@ class MetricsService { const relativeHumidity = await gfsRepository.getRelativeHumidity(latitude, longitude) || []; const priorRelativeHumidity = relativeHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); const forecastRelativeHumidity = relativeHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - if(relativeHumidity[0]) console.info(`Least relative humidity 30-0 millibars above ground (%) @[${relativeHumidity[0].latitude}, ${relativeHumidity[0].longitude}]: ${priorRelativeHumidity} => ${forecastRelativeHumidity}`); + if(relativeHumidity[0]) console.info(`Least relative humidity (%) @[${relativeHumidity[0].latitude}, ${relativeHumidity[0].longitude}]: ${priorRelativeHumidity} => ${forecastRelativeHumidity}`); const groundTemp = await gfsRepository.getGroundTemperature(latitude, longitude) || []; const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, 0); const forecastGroundTemp = groundTemp.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); - if(groundTemp[0]) console.info(`Average temperature 30-0 millibars above ground (k) @[${groundTemp[0].latitude}, ${groundTemp[0].longitude}]: ${priorGroundTemp} => ${forecastGroundTemp}`); + if(groundTemp[0]) console.info(`Maximum temperature (k) @[${groundTemp[0].latitude}, ${groundTemp[0].longitude}]: ${priorGroundTemp} => ${forecastGroundTemp}`); + + const windSpeed = await gfsRepository.getWindSpeed(latitude, longitude) || []; + const futureWindSpeed = windSpeed.filter(result => result.time > now); + const avgWindSpeed = futureWindSpeed.reduce((acc, result, i) => ((acc * i) + result.value) / (i+1), 0); + if(windSpeed[0]) console.info(`Average wind speed (m/s) @[${windSpeed[0].latitude}, ${windSpeed[0].longitude}]: ${avgWindSpeed}`); return { priorAccumulation: priorAccumulation, @@ -38,7 +43,8 @@ class MetricsService { priorRelativeHumidity: priorRelativeHumidity, forecastRelativeHumidity: forecastRelativeHumidity, priorGroundTemp: priorGroundTemp, - forecastGroundTemp: forecastGroundTemp + forecastGroundTemp: forecastGroundTemp, + windSpeed: avgWindSpeed } } } From 6f6c12b4a1e9b742b1efb4b669da518357db7063 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:54:03 -0400 Subject: [PATCH 02/13] New default precipitation rate threshold --- SprinklerSwitch.tar.gz | Bin 0 -> 44580 bytes src/services/rules.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 SprinklerSwitch.tar.gz diff --git a/SprinklerSwitch.tar.gz b/SprinklerSwitch.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..35a50ab4d1967292707830721d63e5d21301d6c9 GIT binary patch literal 44580 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 literal 0 HcmV?d00001 diff --git a/src/services/rules.js b/src/services/rules.js index bc0ee22..416af72 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -1,7 +1,7 @@ const configRepository = require("../repositories/config.js"); class RulesService { - precipitationRateThreshold = configRepository.get("precipitationRateThreshold", 10.0); + precipitationRateThreshold = configRepository.get("precipitationRateThreshold", 5.0); precipitableWaterThreshold = configRepository.get("precipitableWaterThreshold", 50.0); cloudWaterThreshold = configRepository.get("cloudWaterThreshold", 1.0); From 985ab6183186e9b1c1e520c658fe44aef5dbc74f Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:05:33 -0400 Subject: [PATCH 03/13] Get rid of archive --- SprinklerSwitch.tar.gz | Bin 44580 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 SprinklerSwitch.tar.gz diff --git a/SprinklerSwitch.tar.gz b/SprinklerSwitch.tar.gz deleted file mode 100644 index 35a50ab4d1967292707830721d63e5d21301d6c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44580 zcmV(wKG%f+~ut?(wl(_H-dBGa?lc84(#7nd!L_@Lyl~0zt5ZVY&2gG5ZU` zX!dt|G!6j_lLimRyhm2z>h+3XoO+wa)_ z{a$^Y>$A3jL7<@q^NNgjBt;;wW_$$iPW$}-Sh z<>SL6;AIazU|T4bnTJn**;aG|LwTWETI8gX>gaagcKy=|PW;3Jl6Tv}A3h3RWJ$S? zpK@P*&i&& zLQU(P*ZAwc!H=He*Za-hN8b&UPr1)eU(5uuFNXN|IT!k*^L9D6I1+nIzY(E?u@cz2%eW?j_rP8nsYhFRV3ZfeZkjli-}F@ zP;(Vevkf^0eva=^Rs4?+*Q-4Il!L%WFtx`=pks49j*Gdk0R5y&3>rItmJB<+>HEN zXa~AoAjxsj+|OBCuQTsc?w|k6eLMi%)wsZ#i_N@3Kp%DeIF?7rU4D_5Tz7e{kvFRmZ%b|4T?|tN)9M6PW7%B8+|4 z|KH-;4#8&LZOT6`crtc-tKpzeN19oG%Eo!yq1#a(%TQYOdfNfWM7M7H_2GTezx+T} zuIlYwfrqD6ZA*Qj?ykhcPRSo?Kk3Q)LFoy36F~j*q91pS`svV!RJr}~xAR6m)i7V4 zqJDH0If&K5M?sQ)ido_XhM$g`v!9Q?ZYsI|`z0mjUvqJxSWSK0zDul@lKbdkN-2YeZk0ifg77Q<+QQ6 zj|&wr{|S}>BARTxI=q^g|rarW?-H)F}F^MbD4uj zu&!|Mq{Jx)&q#BLNk^{8Rqh;|sMoQjdJOx3=-Y7PfbSVLq=hcg|D^UQ;m=+@q4?pe zFVWjo5@0K(5tzEH`_Xl(GiN1Vt8vfkr<(-78go?*zgJT)3(ebF8jI3-TP_GxM-h&go6~k5^6~#<@^k^jsZ?Doa6uoRlj=+- zKY4vY1Xb4#EP1Gy&UN9d@5?B$-3r`S+xz-4k^{!KNZhTR|5Uc#(vUYRp##2Wl#qX* za;$_hc{!$^NMP4Bevmp7h@e%MnqQ{wT zBvvJ(9f;1dq#gg zk=cEj-(HB8oQ^c_j+gwqU5EL=lWFxIV^<)ZMc;>v z>brD-M0@Wh+I6p?tJ*GDlwp*Wq^D;t>D7QvBK2fiu$fK;AlPmu;KVxp@Q(~b+}MXS z4?wrnhq!4Er0#9+emi6e?sa9jqv3Zo_@90~$^XkDyzh$RdHoMT zCb%B3C2|kRgHx)u9Eq!`--s%aN@zO;?1NV zPp34WVm*%jz8&jY@Fl~%hV(S3`DSd=^Vlv0^WKcp)g0HmhVfzq=gla6(|HExNqFE1 zu0xbzw2v^{fHF{B`3UDJB+$h%3*@IK?*c9flziUd&*COei>*$IaaW6FrD(y|9=&Dk)qOmHyPq{1?cy{0 zX*KSw#P~r8_S8BT?dxKd=*m zx;=vUpHnoc@j0OJT_KkEi?9>vJcQ}u;l+MF`=}p$Iu*R%eoErn>D%~gkMC?Nx!bn# z@I2)~Jm`4ZL4{NDCsaMLdr1O~r+EGovU<1Bbf$PWwLwu@svP+$eoeMNPU`E-aJoGl}zX5fIsKx zz*lyK;&!NNOodE@eN9QlI9I3GoS}=Z5W75o297)Yv7X%by`8(5G%xx4t1S4!$3r@k zm!?eZzv?l5tz^_ceyZ7lyL(+aPac} za?ZN!Q%)G>B@SB3u_IbP%cM)$Cx;J<%~>P$ zJt{DaN&NbP)iZ6&*RE8flXo8zT@QVFbOf1=hdKU7IJVp1Q`;|R*(#UdWS{vmh=iM0 zKpejO^7H?bD?W?hqKQ4o)aQ(P`7Jr}>8n#Kc@VYF>F+x_au;owYne^zpRj<8Jhr`}ypnF5ynD3%=t|SBfTA^8WJ? zm7jW2yK6Va^f`A#wdC>Zztg^$oD$Z-9ol=lAV%kXYM8G$OS$fhny({#fo)+mX1S?{NlZuIQYfK z1C*a~`mRrUFK7pH>!-x<_1yp6RoXgLv4(|mf6aY7(;eO~dLH^))(_(%$K5gqFF9BErW`a5um<6$Hz9*UuuN@CVy zuVO8Dxq09vuD#+a$%Nn@Q%({LDM){MkLPw_NAgF{NM`oHpK=B0)3znggusvetpD6N z_>=hWe(L7^aJ)4CheFtX{ZFZwE2q8 z$LI&4w5^A&S}|-vzTl-NBatMJ#f!C-ASZaz*|BU@$R2kR2lHq6v6FdJBCg+Jd)?S0 zbv@-{0&yS3Nd8U#N&K6I5+!S63_}#8#c@+&*T#<=#h&HjBDp_&$^Gc1&gYMxVl=M3 zP8HBFE>Uw`+x_^^j!WrYJgkZx%3N0T;e4=n)V@#IO-6wyZt^ZLQnOY`3)3{BqsqU; z&r(Y78h+vdAJ;o4Z5Apz3Bf18;=%REaTK>I#Kijax3gw%BTn14T`)4+35dW{Vl5s+ z-=+|zw5Q@v^gm`Go_we7b%pN7ZN*Qq+1q7~Z6Cd~eCAwm|1)hs(%Zf^j$7CNf)I$7 zux$Sq1pB`J_gh>E|8+}`2VY-L3~uE=QiNeNE|_=(@b~rK-{ea8AM0cxcH_UA0Nl!d z2!dk2FXcaieeeI@32mkyhDEH4C5O{71H<^4?++4xWv3;i5ZFg!6J8llEjep6lvHl~j7g+c%I9}@i zv9o__|6ve8zW4uca%DlD!=4DAb7<+Y2!j|{F8{QBx8L*NbFKiv@FRjjI0hrz*C!eD zpOXyw55N81U-kXE^*SE^eJuyzrT!0Y`~Q*s$G-RfZ*!3~iWqf}{KKwD)ckPufE8gcrR=}C4#D$n_hEOh~Ib3PcA6&(>edX7gm;1qsyx93aEoVC! z#Ea#~%qAUy%fUG$hvu-8mpd3aFbCy|@heudUB5sop5*FImZZQczPH_!#8WKKcGEns zEWto`U0uym3}V#2Vy4iKDreU#LBwRNb;%V2s8x>U2Cfqh|=@Zm$U+VR&)v-{=8{b0`(JV!4i-rUmiTg&%wyUhg22> zc{?Gz%kzlaucw>%%#s;meU{{f=w6ay78A_>@Jp`XX2Z^%&ys#e?r+Ji5|0<&Y>BnS z{X_P>r=!>(4j-Dfsum_VXK9X#BS?#6Q-bZ}tZv2pG7Lpd8WQFne#oFdE?!Zd!Lo(z@n4d^Ox=Pm3t@zOoI~m&kN?4Y&oojDoBYyKs!hx63rJo##kkJO45ixZM6&q`aJvV`Dg@ zej-HIzQt9t1cdZ!Qr;Gl&nskQbA|LS`J=0rV8rUVP>5B3{KbVrtd~+r{9&uN;zyQX z#`kHL-OuoYNfV_+t{;AY9&#y-QbeD&AHMBg4@ruJWoKF-=_pwfel<8?D=PG9VMlZiCY5n zoE}V8?jDMyKaS+!Lb15ur^^J&<)BYbND=qzo+0mYWD;HK`MP1`Vxw%uE!2rtnZ0VZYA^mgK=F`II2j)&e9x5HPCatbWnw@(XC96a}=YBXi2)Tb`1Wy3| zGh^L9FEQb_huqJ9f)hXq!;lPiisi%S??cD`UDwv7i5-rwhvS9u9|R%D`TqaV_x<0$ z&GpAWq+U+qZ%VPX7IN1o1T*(Kd-ow$A!{q=+P42W`RDlElaa(Pxj*Czvs3T<9Cm6~ z4lTG=>=?#TgD*!gaz_K6xWT3^=Yo8A{PfwTJ~y*<*4#^oKLk#)5UJowkMj9|Ik)|U z*o`(9vL3sa&c*q*zit|!v<6>vNo|3{PAz9;9uA*Jv7|C?ON;O!w9d_8>5 zJ?wjOA?wNy$rRs1x|-0ok^MIzo{UV)HR+ffk)>r~On zWc}sKG4oeP68M5V{P*0|WmQ=#h5@LGRXEV@;qNi$PUYjcaJnnOlj1P?l-G)-#v*lw zHx1s#wSNARNXSh*76nl;Vpl{~3NttBIG6ZaiW~7*dOd^B!N<%!+!QjXFSEBIO&U;@ zYgKw>e-$;S@SIb4m2J|i`V^)}W<|=qCO3$Jh0$%ZUYJ)LhpbhWfQpugXq{)4U8V{nX~*1Sgr|!xSze*S82Mc8S#Hvw>qOxqNQ}nmTbdz5B(XugOTOeG17DdzmTjk zy3R@L5x$?3d-y3U_jiR1?#sLsCz9hxPP?+=(Tz0``Za|T0uT`$yv%CLF5U6+t|>b{ zz0PY2JJrhh4UWq2q@Hh=q)A5{uQ&4CC+#s1ET?9F9vD?)@B%O2o5=1n{p3y5b+aDg zDmT-!Qt=Vn(W||1K|F=M#Ea2ddD7uyMNCCal1m3P!b?H$73( zk-n}*sL>qE`XgsX({QOl*M&*#4SnN^(TMHNiKRVsqZ8e8sUL02#fmi9D$90v#juOu zG{taYlb?vDp6!)fh#fLj@x4Pm_CqG$Aiuof?x3Lz|Bm}mO0=l!gTAM^cDIDoxX}0k?crwP-+EH@r6?~ ziRX{)*?!{2c#65&Qll6fH|UKjabIf|Sn{cZ5*!WET>mh#2voU8T43B?0;v<;ub%tQ>#gHq|c3UXACnC%Rn?o0G%JR^97bv#y>mWG6nNHZZa z2l%+5flj!R8+L6`Y;caCgrOeJquMf{HLwFtx~(o@-E=V1)4FN}_A8VQYLObz5H!7y3@HJp~suo$2UEA!Kt)Z4Jj;m8CV)i*o+=N9;~ zuX3W%k|N)f*C)=)SJNv`kUin};nnTl>^x!!Q4foAV71JJ=Nal8!fubiZnkT8b(Iqv-(giPfxA4F{|>9pI^;5 zoFF=o|K#z_reRYdnk%tNYuM1si+Irms`F7EWkeDyxVypOouEs}Zr% z0?^3KGB5jM0OeGRUg(bFecNEs^J@L8^|wp-4&1o(_U7K_GWmW*8bB;PAjya-^58o3 z>-Zq*RMk-hR2ZVJ21}+hTT}_WWRxPJUsALs&UWYJVy{;p_V8YXD}ik-pe9mt(cpV3 z_>$lMm6@{l{l8kjILCE~8QGhgdq*hZe%U7>y3q+NRRf5oJ)U&)D%TJy<;B_vaCJ80 z>#UDZRINEIF>^K_jztMxjYw!ToXSn7JqH94aph&J6wE9&(&68ucRcH--W@90!rl|w zyE#lk&(+Qpg@vjK+Vy#Bj#qt_M#u`@?e%7{%{NM9b*S`P9$XIct5VbhmMF^n{DM-#2Tj8tb>5 zCE5!ncsQA9b&iieU9Gg3aX9nae0kyNb<#4831XKj09R@^38~DZw%*1|LBGXHz{asMmbVQ`oP}|Jk2$CV&raUPV?jN3 z10qAY*4>eKgb2M_QjXxpLUMFhNa4LKCEO64Hj9fXBM&uv61CO{ON$c|2=d+K%p<~@ zGwy-?S=n3SfKo=sVc2u}?XXNS5M@MFzsHf?-XM=#zOkf2osse`w!A;kkg}_=jni+r zc5!g_0@u(tb8IJemnZzuJ0b`v#AJpm`CCX~+@OtpJwGcOPOB@eDbycxIM8#4L!W}w zY5WGKmPSO#cHjzzM2)Gzn1b_Nwc22{`JyW3n^dK2VV7tG#aae=Ck|NGb1*VA_mSV)2V>&@G@Oj zB4fK1j2aEbD?@;aBb))2sz5a(0bbTOYtrqS)oCzkyQ0jF=z6gUO{?_*qs$fzrv<%5 zv(8ycsWLz?lAYPma@AT=Zww)U@u1`Fc3u;_!psn^Wr`suF3PPO*a1!4!0pdM3h8A^ zd+pW?%1g?i;-W0QSPiTS*(LoMI^-QrcMs1SkbXpTSbk6n za0R%g$?u-~zc6zl>egw^Lpn{i(LfAl8&^yY2!8$G=2epx+xY@7vV0*dq(?Iq_IViB z;~4lOdN;qH#hA#!vjqw%@|VTKk0f&%F6y(X5Nb3d@r|i9<_phy?MH%({BpjH?iVU544J-BMpyx6p)N9lLlU(?* z7XeRNEmj#3iUgYiI_`|DQD14#BX?%`+*$#;MY+m5qx`0Wbe%U(nfxoWH$9G^;J9|& zLSMfcv?x77@%ZnZSbdK33DeID6t?JJ7J!+F>hip82;fA}MqQQacB&O{8L_fw_w{KX z(FZleT|}T+8Ts@SMk2CRW?Qmal=&e`T7l8ymO6zrRJUx2{E8nArjxhlP+t{*jH>?% zB9NWpRS=UED%XVoy<=Q)Bho8Ua9pI2wcv*I>-;3@n{LsNqpn34z2!uq$F;nuwxpKR zD@H6jAWM-Mstp9KSNdI&28NA_hUr^)n1x> zrD?hP5UF$X4d13*okJ$oBGIphwAja)X_s0@Whk%NQiorbLE0Dc4NtQx2!w;MDB@pH zf4sxo`tK?@rt-`Z4hVcNfAR?1HDO8K6*8PJi^*g;GxIzdg89CyAsnmJ2G#xuhoWk} zHJ-u_*{4_*oyyK^syQgVplE@tLZhXp*sEH5&5j(a*(v2$?R?d6O5#idb+Yso{fv~7 zzc0g+T={Jf>A3;KJGmIg*q(6w_~oshr7z;tM<*IogmJj2THYQ;tQ$r@P72?!Rb3K{c;udJZIv8fSV-La z=K~7wX91o=|3rYED}1w!kny#^YKB9Rx;4OPe^noqLz3ui+6}anrzqP}B3KltrcJwL z4A$DcjsrEA66bB~>8j^(0GekQZ_}NWNT?(Vps#@XzqgHuhhK*7X$8p^;xbY)->{?m z*Ebo@Ab%nt&y>B@pa|@2hHm=JCKSNQU?flBHtB7kva&{-^;%0Uv)ni@F4xZ5pHYUQ z4@g?x5CD@O8DnxYvl>)23=M@Ef_=K_GDwl{bibnF`TY&b_Jq2Er#Qm%GVy+D;|b0S zqHtE?W(84h6esd*A+>>^XH9^12@nW4bT$H^E(R7-iZ{f9o za(xo)OOW9VmupaH+X^$J^-6v*)(CqDqkSU3@WyP#rF;Wpn+)enjdd64cKd_5q7iTn zC_@y7F!RpX=7y>Am~dTWAiePc=3qI^aOwk2-mf7@=CYiIN6hkyoit78E0i%$nAUQnM9^a^GEq%sQycesuw^fOUV^76)o& zzWBGEzi?cAmFyR{oh*n7@vBT7e}f?Qb>vS(;(F1WgWBMlvz)RVwo34H)FL^!G@Xg; zyr?%icH~c{riQqb6n3qZ*P@Vxzu0uhX+Hu?0xC7?o-i0906Fl+6CRpDgZg+;wYkyP z^cViO2DOv@mY!xdd}Y8EHYY{&j(Itg^eIroy^t>6jlZ!jWI_k z)UBsXiQBQoEqthC0w&Ne|6MT>o= zF0)*H+F`ncTNzn4+^-Eq!iW&x(YwTe!FXYsZ+S+zuzgSbKIL(?mgW5y{_KFqGO+u3 z^U@!o*HbkJ!1zsjr9$%+wg87OL!zHp|ij!iL7wK{Xri^aBIa8!bfbs?? zb#MoE=xAB&`zOQgfG4cp{=7LMuA$7- zs1JoD47KZ3uWfRetFWfem74q&y0*tqYTVMXNYxX6|k!f z_=xCKnFZq^n?`vBmYNf7p*l;xI4$9n6-d(51q_t((Dxd;p05kPwD-;3>(+d`FcUog zG>K}r+1S^U0?Af@6<(0+nDK&oP4R#y8Zq(N`VK84zH`IKfcgr1avT9Nl*kVVGaoL|Z;n>yt?wBOlW~d9i5aI|^lR9C* z%2vHU^3r>Q@jPLcyrXETJw1lTz8DD$!Qkocja|K(-M1+k=zs%D$!xAWtS23bA z=?s-lP~>nl7?5-G{%$438rw)t`$$C`J6D4<-I5D|v(=GLcGQde$adrG-HvsC2hU5* zy+S0I>27s@N0OYs69ijU#HPbGw@tXGm*Kbcup@SQlBSr~5>>#vRxCXQpR7She-yym zRxM3=W`fSUD^6v(0Zw+1&QKB-0^#%PAvX$ZIHY-o7m+aG+x!?86si(h?+r1Q^m7hAN&FebGv_9=);k9L`SOD5Co85CxCK-3lGAL` z(bW!o=|Z_@-g}1NY1_W~`i}Vbc)SnAJvceWDA`jS0<+&ge}V8@7c6WUes!&-@;5=LXr&qK0N_ueW(ht771i&+_0NfP09`@CK&ma52wWS9SIl*|h5Jdmp;wAiaW+PwC1p<6p~x^VNP)ET+ArPAKj}r`gv;?j<#qzTdj^&*E7pvvyZoYwA+$cO*sjf`uNxcU;By zqe3F}!nLNJYs}+U@hHV9B%+z~AHf|W@HW`=X}0eL z_=i`yWIN+*8tvG_PZ8W|78GlxyLOtu^);-+5%^@C^x^JiTOkaZvj(|DkXq<<(Rm;0 zj_bS@ssmuHhh7m>Nxj|6^BgVXv;}s@{jn+`=yX61MR?v8M&m(=4%W8r^Mu!hd5-VW z4)K-~9g@$Q35(c4k2~Z`mf%j^ovE#CnaPubI=->vpoGr#slvAjM+!NSi}c6MB7q0A zLeP;E!(RzVA2KS93eHJ7TjoHXaFKds_8r}kqX92gq(EDA20d26Yk*URnTU{wHd~yM zkWVf6e08DOwQ3D~L%eQJpjWBp-<6xsZnF6fJl$`ud3<{_f9rZ$I)(`q{5AMiwk|6MwR#y0uK|rk~5X$@-8HJ?>qxkG* zBHcgTlCm%vneRvB+s^zuvG7#+ygLVvz{dUz|0IEllr}Ct%l5@kJSPBnp@O_PWZJ#$4zt z6%Q_J+KjOM&07WR)RVgI6Fh4!UY6B=CD-71IQ;J7u?L=>1GfErvxty{vjp2MvDK?> z2x>BK5M;L-5;dePYT=*)MOfQ0L~JZ9*ToGd24lfzEqY$VR1c>{YOQZ}J!?uJ-RjsV zck7@ofN1|MnLtnHB9ig**^wx>Mm)J#`3x+P9;(sy(Jt>-;a_k2CK3{yca3;k!Ks}tvb7eQjed!G3ZvQ#x zlB#%n1T!1<+wS7(sQ-l#yQLP4SR?s|m;iKMvZTm-lEwJ6&96-gH_Bt$}1Gv zGS=RTtIW-IS8oz4!K=67S-646Vx2Lp)l$(YQ3FdfEtD)|(z4#?{LNczmn0a5r~CU2 zJP3r5Ld-d`0%Xk#o|eQC^_G)dIqM#U1CGT&!q zOd!Vt&YQVYGxpdKV+2X-DD0?=$us51U83EJT{FuS+MB7=W;h_k)>}53x!t#VIcj&^ zUwCqW-t&&l?Ne&STdRP}8f~)EHnEUtySZ5d)p;>2b}5utaWF!RmZU>%28%ABOsbP%_}2LE z%ii~GGhr`HU;T^B9Ul)O-d|95z_ZlN?Dx&$VK0EnuupBMHb<{}n9^%7?#ilg4Vj6` zP1keBP~D-0L7A>vy3y_MgSJWVxCc#mk`e^Hoa|*N!4bRRz=^^zwWyQ5$=wB zzNB>KjFz8sQ>oM2yj6=IhCuIQ>CbDN4$dzZz8EIizKaA`-NvC4U5G!|lYa*>yLKMy zDl4?5Ra%ZsN>qT-__J~rx*S416(%1lXVepouqrOqjscuSf=R62-emGLCSh zq2m2rA%%UzR)|cKRjbW;9|i$m9JBo~UDGji!1bAFEnEa-*=Tk0qh>g2`)jU6;Oj9o zbVX=Rdk{nMHdL()yHfzuoN9TnGTUR>cG_<~$LjUOJ{F_PIotOft8x)T5*^t6E( zufpJJhZ#hZ%_v=c%w$Vap2`@?@(yosgq6u;Gs^8@0dI{uO66_H4 z8tD?nR=a~~8WliZiw9}T0^XR5dl#9XbivESzH8yybv*lk%>GFMx@`k?b%jhBp~;Sr z*}PAvUC-4k%(6Znbc)M>3PE&2_s777#Cf{ojS$$>s#S3&_KNK()tRp*PIZcJBC@6~ zJ$07vuloXBs$ZEdyN!hBI_a9pfZt{_PS9k-i^F{(LvX_h4c$disI4Qh?hEU&83Fyx z+NzVid3Dhlvn0G;uIBYRq4tP%wLXs62|t_Ip$~%1)?!HI@s5a<$aPOd2m!RqRl-pm zee#ys#%t;|n*z9M2(Frro3^vR5DLArn5J~k5qam}w;pEvtB`?wnNN9Yjv@|AuUj74 zX50o)M)1)r9}u-pQ{^h=V50&{Xi|qjc^=IOWN5h~yo>XjWmN<0#Z>k-%gSIlot2Ba zOwVeVVXcQ(^vm;vN^=P$+ca(Knh%bgOoF_IOvk{}#Ow4|dI-i1O1O=f)kb?z_BaiX z4OdwT7wb^ynY>NV1{qc%a}qV&`LZ{zNtOA;EfxK8anpe}#vGGJ^(oR|*Dwn5&{Az6 z^n3&&c9JW$bl=2jzJv`FuiVMhwsVN8s@Uj}& z2mrc+wpw~i9Q3Jxo`;v7A$&G6Nkb05{`$=&jCA6;m1Tjx+`RGZ^%Ki2aY>k zZLsx#YOQHwT`+(XqvUezhzdH|DZH z6z{PMmvJ0~V*jX+B6wLa2-chDeWylxHJx%H!5zCsx5y!;JexE0)Gv1YHo!T+!d0k_ z8nNV;6QR>uVs3Bx_S4kQ_2JFbKMg%!qI|Ldw|p<*kH9Apsl#0% z1$)EXh0#Ym8Cv#g3YMI$AyBJ9JrBa1%5ib+FTua?h5LlDYa@s|g7{a_oRcwonvN@O2=I3YR*7f~tOdZZ zonGix#Cp`C0f8Ijo%tMVR8_&FLJ4dGZOtZzvjKpXh-hR$SQCyaBS+Wfrr_m;JlCr9 zmWt$3jdRH!0dP$3G}|tN^fHklUv$JW0%Li#Z?)T?Oq+S2L%TW; zuG-=r% zvWu{0)9vu8k;a;X+U z=hrNZui;It2Mx&X1l!O6i`$D5*zL|Xegh2G+Oi!9ZLrs?02D^(#92S=EaI3B5%kj}h$Yy*)=WgM7n;BDyROMrC@iLhV{_gZf0H<##ubv<^C< z1ye{UNwCggV|m%>J0ok3fWjyUeaIxj$*`Wc6}&p7IUrwjM<|P`47pkQuMVr9E%U$Z zN&*iLk^YaA0;)qO1xA0^uaJSWLPbYr!sTXMn-R|~uw`bNruGzaDipRv1Q$_KDJ zE@yc**lhvjWzZV%#*iJ=J8-8_W=Fh@b8BycdbVI`YCL%CvL=q!piV`q;U3Zh*N&_+ zn}{iIfa$bF8}xXEl$O1oSa#%=L62%}>Lnh(!wx&?mwJ9=JlkzvA2>dOyQ61zmv)*P z!o?f0Qg*8?VhFk7T<1s;j&(HE=uSGJ>M!u_hzX}ibGjxGs@U~Ik)IB#mc{B&XA$O` zq^-$)gK}UPUUNjJsVURefUUjtg!^rF*uyUG<+W=s-lN~7DKEzq{p+?TeSvP`ceKm= zx|W9UdTX)+nsPe(nI;|FvX42n%nR(MhhZbBQZ&^8 zEhBAFs^!h`Fj{e3i3B#DTobz%R}`e_DDW40W5BP5F2vj)hqRInIShTH#8%M1lIG^< z(oyyt{CM%OixKm-2cd@<*7M)e4zA`X%5VISPm7Hhr zD|>iTq3cEXk!eS@ilSAVyeNRh#QNKM0#OEsrZ}L^a(S6qR^}DRvsu=DZknCe}y? zOcQSN?aG_C6nkmqxB9Y!(P76o)=OaH*5h5H3#;hjKh#rE!$Q##B5)sW{g?tnE7UY zy$*X}3BhS#-08>eF$$Y#vyP2Hs>|c!P6t*e^wdULi-2!7V6x|o>HKp26?YOpPg;F; zpU?}7buaD^d12}9bBk)R^Gxh?^@S2jMzEF1d$xhP%}cy21SdtVH~q4YeoqeG6&&Xw z+rygzc3V!K6Aw8Z1hP6SfTwyJz8dFLtS zYKyO!4hz1zuw~04Z2dV~eNPtu6(r}1?eOO29RC_iNC4AX)rVAV!>oJ5A(}V3qjEthm&|8ER%(X_aHE)s=F666~Mx)k*Byh5B*dj6(Cc*qI8-rfl z(;T_H@%hml<9Uf&&HVLbid2slzkQ1Si8mnyuA3-^G|rtJg>lurzGrLamyjI<>}ru) zH+(kaiRa<0STV*c0TMkJUzYGzfK8VWmB%aYvND~59Hk}4b7)1Q>QrGHQOTALU?kJ= z%9J{pRxPtXX>!yEH5k6Sh(>Qt;W#~IM2P=Ng?)C)PPp(>OLnoUsK+<{zQRTIcTz!$ zJv#4vZ}n7lLbq0qHKDId1<}}TEOT~r&$Zf@4!3{qSc+#3INzV2_)@jAW3q3h(hH~S z-i-eF6IgF;&83q%Z*IcHGahfneD!p|*8@15CwFVzXU`(LS?ZpiK6U+Mq&v>9d97-F zen9;OBYgmWu040d>zfQSSV2TBWQv|O=37&r0T@GX49PyDT9DUm^cXPW{3X!T2UHaF zxH-k<*MV*W463r-MKm$nW~+j(>(dq7oeEZbJG8p@4?3>9IhTgIHyE+6yq5l}i;^K& zk|GzL3r|8P`_SDkM#rF289IKDM!#;D#zAaW8TG&+%@s>p7a~lN_u6PAEJr1Ot~U%Z zWXNgHX@Kovx>ZzHXI}4G zZ?glM&}RGd)>SnQyaar})K)=n4vdtJ!4ppvO)*5^j2W$8ha@sWB@KAiUOc+icB~ zZF{CQyE7rc8Eu-cKm&m5 z*rVpqH4(<>7bD1@)W^-9kZ)SSL}%*!sKVR0ML^58*`T9N1<<*Hip8UHg|c7b%3*5m zmpxTa_8;5rg&VtZ)^>**eWe`rhTM~>ywjrTl->#D_W)8rt-n=&`{{X{zAh_@`87C3fJk5B7u_}0#G;+S6SS`yLfP0Y&_5O zXt>1>+lpQWUlT>uyOhJ}JB&Nwj zx6K2ngK*fb0d->5Y`SKnGpZqWD{?TZS8OnqnKrl9D6pnYqKHiEWXl>?OmzYn(V*g3 zAh`K#ihNUl{WFNXPwS6l_{R-<8H3p)fVLR-{yezP3( z)@@Yk!EjVF>JHqin)5ZM4UvW!s!_jgwPvqQ%{tyq?pn^l>s{78)MWAJ?suAXaD&s@ z$18Kf<5yEQ&o#+W6N0oxbx>QC(8;h^nv8U;)uOuN=CT7Lm{&39JepsIWja_H9;UCN zZmlc%AzBOvodF^YgIdY+Ud+gPX$}4jl6HKk+U02#I>H&UA*TZe$81M~QztvfuUqSO$SAoL* zv{Mg8e5bppK^2=^EScpbqFUu4IboNY94rY`3s9RWH;kZek0*mFAk6V`WA4>CE-$VZ z8kmn!FfUQ~-{12&5rE&t^La)%w}}nc7HhrUmSkCDh3s#-GV2EWVYe+&qavosJEK6^ z;Ch8NqndzP;v8-E22r3%z&a`~CM96Z^`&*QSLTRf5%0&&`Jz|KgY{C8?JzwhT9>QN z04zc}YTiE*I?YwQaBid*IN2hRG+S>!TzdU9C3K<6U1@shzpqNri)*;grnlY~13Q4t zgkXDfYwT+=9`hT2HO1Jn4J{~nUMmXk%YTmA|ClI=fj1X#z z?M*|*`^D*GL8xFD#kRKGq8nP&qNmXOje7$@xA%E| zux3bMn6^SKb~*6Y3qsL*I^^O=7~w5L?os4$tOqKFwJSlnWr>C*$?&Mi=?!zDHfA6+ z>`&QJsoSb(6}l~t=jFzLe#z~)S(D$X+B{vH(5 z9jRJpwakZWV6q-bRY;BdPIb{iU77Qc&N^`B=C$LkpP`na2iNu*!|zIvPa!6BN&Xh_ z+tT7ItkSijz$;-@6acz|vO4e~K=OusWCJ)W=7m{@SZoN#rvlou<#L0v7xiu+PaPew z2V4o!EUumRm^{|!7+TJgZs`r125ooYU+eOI6aMoIm#3TXpQI+PUE%0;>8^|`=$0f^ z>6*?$AB*?stW;gug|zm|d>e2gDAQPDRnG@PsQV_q?sbjQ-0(wOUTU)uf{@bE?@gr+ z%Qf39Np?s*HuxQpXzNiE*T`~!!X`Wkd3Hh9$3sm}q`L-4x}9bB{`@8PB+`78PYovX z{5Khdi~CT&qoI8YKc)PWr?;A3qhX*j2K2f$v17DWLaXb06wuk^$Eylh3gAdu4kx1Q zE=!}xA$dVJYAm`EjEM*|y0Vxp?Qo!NB1#_Kp-;}%KU`ZJd52y(hMbY= zaG$P>yk%28rdI8H^O7C_p@`Y-iR4iY)^Vt=<;?*M?r}q=Euxz~W?TH4qmT_}nnSxQ z^=9>IzRHl!tPPAXJXxfQ;L}D`s2S}y9DbgDgC%4~mHxjZT=V)h0=Lebo*=sxeR};u?6Q<9rB_H@GnY`GWZ5h55P0=({u(?&C(r_NAP=k=g70)k2h2t;X0m3zEM+W2gS z_V9>vu*wWnW-K1m?1_ynQ7SCSlX6cT)OnTg2Cv?R>|`$L@2>w7HDIrxlLXAjdJeuG77uN)LBH(U;; zYAvWrcskS$^7I5*2wnp!52FTU3S+0Zc0VfT{-p8w!xzg{@ExSp;pFZGJfk@#>UV z*EqXHHO&#!&o&(9AL>W(_DQ`HDOjhLUD`t`z?ALeJ3}njV z|ws*gCXD2^vTL*VqTkFemZkDy*j^^+hbE& z*YR$tzvj3xhbRZDs6k=q&OqE*2rqn zV&F6?JxdCs@u*(zIYXgQu2slVKsB+>tfrcDPr^{rdn0*ovMc&*wm*>SozF@+g`JT0 zgkA%s8a%j6Onl@xn4;p;~As@t4)$2-Fjhr zTU|(zhEfMbPUvPhQY>EC*V)J(@X>$+C4W3eO-LlE*@_!m$#j#1z|IJru(fRO%f1#n z^2^fB@3q}HF7=OWc~`0@9z7pET}buler+p_-UN7#wd>)YapQ@gm1r@Oz*)pir#`Y< z#s)T+T87r_kyrtkgBWxC-lK$$)MjHMSua-de7F!%f=9;k0YUBTn`u6ej^o?V+YMmS z3sb%nWVWMZ|1pRX`H;`&hVEHjFK6d?QIAb@F736>R2It-Xg4D+?ZEjV)tI_*@pj)r z3l=$)c9L~Cipt|;uwJ=w4h>kg1bh?mr<+bB=?d9ca|o4=CaJuQSQ`KONKTXR9*P3` zP*mVd@x+qT=M&n`^N$K8MM;!4a@tad2k3@pHt32-ch+RT#kZ^4>J*bfnHCoj5R^eu zvGxNJPU_K)+D>vE-)#1UP&h3Fi-)Q)k;DMRSBA2u7kpfldKgM(?x$scyX&S5<&s_d zIxwTh&*tz6yzhbZi=JDko##pZ^g*h`BZzNTq9<`ZAD&~sJtRh047Z3fn~~@e-ciLk z_mfwfM?W&9&9a4J=E_4jXu_^>Pqy)*5|{%ovG-2%+1HT4GrrxS&OWV zfd{^jlcu4D6RqNTNGwDTPsuvj?3VuA&Y5 zX&%xK-=>{88>>mURm??&aiu)xG)tZCNBO>B zhGS4)#96h|G_+C%as@_yi)o2c8#01ooLxNbMLcCek-d@Ia*3R-VH7JZll7d3fMc}^Bl`Z<&fc?WMef-hS@L^|%^z-S- z8GY+&FKRvXt!HS6%YD}?U-w)8-~Xde7bW{(oA)GzJyd&erg)Om>GKKeSA9TMQo7qE z=4iL3Mk}7?7t=j5ONLy_`ehiJoe2}PhiO`}-_MzOOt##7GS{~p4No&C1F^}Vsmz8) zr|fp!swGsLUi%G*!B-CZOLEDpfQf6Dr#Wrwmzoy*tsB~_B0YXOmG$f4HGJ{Lo$VL5 z6#j5zU;IfQ-En03Q(nct;qhM2tdr9$wojdb&R=40!#9^7_w*9qLEgUOCI80pPj~%x z_FR9+*AuZ|!ycqlMDkjM5bMBun~%%y?Ta#UAH z>Y8!G0p$)OMRVo~nM{C{HD85v(_-ABSP!RFskL}Ix0fCc#ouk;`}_{S;mYnrJWs2~ zHP!Q8)97yS>(?gm4>;LA?9+X(_U-E<>i%QS-O}!Td9IV| z1NGxsgN6ZS!I0#3$!1)Ja;|fw<6)}i@A!Nu?*#j5|z- zs92K|uo_2M7&QB_Odjfn`!e)^V>~sd0TSu=BFgYX#_;6%`Dm$3TzJ|dwz2Nu zTuqwEAr39n2t*15O>+S4n&C)^$Dlv#%E{C({kjxuogWJNtQB$-@F)fRhT?b>Yf>kKDK&ZXr5(pJ~|gD^{L}bm(9o$2U@6#aJ?sW#3Ok>omAS| zM;g{3v=mC@`F;$OE?|n@Np|i@p--u z7X(8|Ogc_;Ad#0KL!hX?b=M>uPv;H`Oa1Xm`q{SE8m@5f>ABH^>8>505FI~SQl1RE z)F0sbGwD+g!OPLpEx8`)E#87y=tZE_^T|xqFwBh=WFZ1aQ-vPc=_U(ZK&Fj7(RCR; zr{sm#nXIPS8Iue)q1NHH=rd*tab_{zErFPIC9?Z&A^N$#un$|yrxS?53E$zUoPD5O ze?2AO4ec|3UtKwC**$UuCM~l0_JSsqh`zdPN{*o2Q%MdF#gwnpB4@B@^PvV0{`mMUg^Yu zR5yc@1^a9g-*eD>eTRHY{F&jmm(GrxKGDN~qG0AIT+g6vG~bx2v0sdq@OPWa^jw}_IV<1e zF8+{9{x{iYzFtnA@P3Z@vcMG|fRFZxxX*k7Y_L(dASX;5NX;0SXVHP~EP6Df;7r|V zGNKL)XuNUU)r_4k=JrTASSx2sGD|2bkj2)~x$){#pznXcN%>$eB{xiryM`6~fC_XX zcoNL<jLB{MU1u@9~cwGX`F3;P2q%`LPrFu2;T%i@yHWvyYs*q&L6v(fhIg z8+{LX+dCtP%_~M0@{aLw3JDuWQC2rdT3@w5{`=>~t6%84*>|MsciS=^l|~4DWUanX zeIBvLqo-#|J(>+JsdPZo?AGUtsgx3pSr1x#z63&fxIGF%$qp6cM5IkMow9IShq7?C z19)2yhx>8VdI>iix)3%;Hh`$h9JBX3`I|B6KC!nx-Jsz|FfTvq0}c1Wx?CGmY_N|Q zbFmnk6B$c}_9mug(*aj>iVUs@#4rKIL1Sri0`69x#Jq8-x2Gh+zgoOP@G0QY{`H|~L zORyB=YPa2c?mUNxAlp>YJj6#%jnKRl+FK&qZM#IBFq;v#nT*lJGGPb1!xo|IaK8En zI?Em3Gr3!O^q8agLh{1H<3V5dFz8LULutIKqj|?u){e;*D^<~PEAt4&;y{z*XK@-wQ6gmAgrMC2NwR^{AS%z+pLjC0E72Mj8s z@VTYueL%#AhU*RC6H{(Z`wm?W_UEMq^60$8Y|YoxwZ%{NDWGmXrrVX0%<&RoK&t_nV9X5mibe)yX=yUFiZ70TD60!{ z{qx}&O6QS8sDH0Rt{+wT@jvg<;O?GhE-O~&**^(F^$yu4sWV@63u*$ zj>2IZblDOQ8^%Z+kY!OmFj<{mSmX6%63yi}Zixcp3f|?$hMs{9Ja1RtBn!OF)|KGd z7G81Ru0SLIf`)y#`rBN)=llG63GS8XiJ#AhXSZjMLfl{o^8*7UGqkCh!c@nzI&%tc zobPjnXnnO1D_*W;nplvqf&hA!cGcFR498Qq!d+eH4qe(ACGE420Cc#9n+$9McSNzV6F#qsv?uU})&HiKVDRsIU| z>dDki?|9w$)h5-iMvu#1`Y$(0tTUZcV>{bAUT#l@`E!FA``5Xb(69VdJXVy&FQYyF zO<=dbYCSxP``5V$`}E7A>iBv54fgR*qvIQRc3k+ieI}hIjh!FOlD{5Yf$m$^Ct#@b zET29h{~PQ6p1$$G{Cp;Q9!!_Rv!2moW(I?r)m3z$g*u{i?R;Es#HFg)JXdE+p^P)i zs=5uAcB#1QMnxdZy&Wt7bWHiueAuy4!ZY?fRfU6&TAID%#mv43#QXtQUgfL5zi>U) zH}Hk-xmV!J*%R2WKVa8^xI~s<(j^mh&H|NT!+2+pHw)Di_|`*KPBBMgcNPKKL>6mn1GzKplg-bUV zo-Sm21PLr)X26544u+D@LK}8mLJc!zlEGY|R&y0Q5Q16H^u4lSt0XczOH^@aRclx) z;F3HVH)&!Yl!SzMhiMZ*gT;9BxW@2egkCa8ziDLsb8)sK_m8Vocm}FF%E7Xq(!{@u z4S+o4leqJYi|pU~=G;J|PwVzJiZGx8ig6(+?PQoObbBu&6Q=gYf*A^y-)Z`ETX7>2 zSbvV zvBn%!XLjth>;+T~Vk#Yr}#7R#CR4?ve6UpKBI`fR<<&?*5F_;kUX0&(U@5BcuK5|*q!{? zy#L3ou);ok6v=;NupcR&1#|rDThF!T4)a+$3}b;~xpnLgMaEl?=Y}>i_Nm}Yr66pB`~`7h^N-Rm)Y&)e~JXWP3^ zc-tBJ1$*=Sj2_8qq?Hy7kb{amAm@v<;KWg0juo#G_J=%!8(^Py)C}A1gpCi+Y*~qw zWu&YR%<-kC%SB`Y&1}Thg1DoXdK@I+jM@AnJK0y7apMo+fPK9gH{K5)OfKDkm<`tO zlxtg70g2-bWI~D@gz4U2&NlnG6&3i1ndiDzB9uPWtHc^C*xZR_cF>~y0h-d)e7%~i z=sjHNK-WQoZvff-(a;b^UPI2k*Npp*d+s;mepQC?STk-v*jn)xa zLCr$;Bv)675ow{@iVwFuW|OE-H%qS|b}MZib=%?g9)Q4opW<)PoC+#0XOH+0zb>KN zK6zw5uoE893I3q}c9LJNp6!DkdICFkXecTRj$P8T@s_j)J3>>sG-{3fFaRuRBehrUCJxZ$>yP&liDPfs2ku{nc)}csy(Sqbc{cK2i_t8xQoG zZ~u<=6FtTL8&CCqRR7zuzj?^p0dVtG-x}!{DM-2rEQX(sS%g{%lvWpktFlQ;lB0E3 z+iu{O0S+z>YpV&%*>avE${dPWf)%*|7Grd)Le^rT5Sb#^a#$`%?aT#va59fYZ|zc zje{$U+M@Q>tF1_b9wx5m01(ScTNwf?gCUuTSdx+e1y7byBiT4I_3J%I?SqvlJEXJ1 z9^lm)q2eh@HM-zUA5&jZ>@PcoVWW=Py!n1T}=(Q*(fZTec2<2)T zu~uBSO$9>nGFyz{dglT=z^SK`Lpm2nBxxHkKcr3FF|EDk7L(G8wN1@_x4891>-w|4 z^mBmWH<8tg2w&yWUBH4v|Mn4oNcfN4{TAi-Z-)1MA@?yj=DBfUoRq%fKM(73e~wG5 z$iKkfWfr|?|B03Pmi|wnWM261oyyj%b1wP^_=AtmgV*%0v*Ojo-gCsEM>*p+4n(CQ zfEucl=H|pi1_zXrx*4j6aG7+CoKBduIE-fdnP#;7;H5A9GtG?O7RvPfU1_v)aoxs^3r#f$^YP_P0p zz^}@+HG&qysQq@Ke_Z={5a_5#JK5E5Ya*xwr zpQ*QB=5|@OPfn5_-|QZNDz{Ii!I!j{KTuFD(?|y>?dL zS|vjNWr+y=-s{6smJ_3a`N~)fSV=1m^u`C=rA-JrHl&&PdQdjYEiyG=HWmbX&Q2*z zVz=9z?huKQqv=T6TDxu6l1MRIEJWDd72)GbZC_pK2WI_8Gy8Q9gm=i_Z|l%kRr^j~ zhx7NhU{~+l)9U#Z(~vkLbpIc1@SoNOuOj~OseUH>GTYB*y(fI78mVK8#2Vl1sEkJj zo5OgEW@uRkNCmE@_86eS&Tyne+Gn^iC5(z%9XfF}1AQkCG<344(CO9+Kt2kot>%*` zX#b>78iH=J&tA{?E8KSe@T9*$<$o}-KUx)D8gRbj!oEexzoc}&ZuW91PrJC$UPGbv zYXfstF7GY}H#*hR%XIt(fYJqr<8~2x|E}|}{Y0;b?@H_^R=UVH^W=9O8Tz*V>>SNF zqZU0I81UCMj}KLg6VBTi6zSAj_HuAd)iwq^13+qpA*bOn9=gnsMiyFcPb zm);XN|KG6ip2%L8-824w-$i#e=Ut!AJ>NYVTLp+rx%9C0;FgyJq@FRWV3b%ZI{^Wz zVu)r0Gl^&fvbg{xpSWmW# zSwn3Na{vYtXpNRb|1dFHX2V%foCK(yCNh)e#455CF8At6MpS+J4Xouq8k9WdG5#sZ zJsZM5EwvNbtHe&ny`|;G1wiKz3hZ(^8DoyObhNPs&6XUOgBu>m#Vxm!$W7V6A>oDq z3EG@Fa2W>{%Af%Oi64z88(XPU#;g$vL}fcN{-y48y|sZ4o6P-3P4sv4pdTMPZyxb7 zjNV-DJ)|Z}sTr>`)z3G}*0bjTYGK-LnJcDMfwRSqBZl;6`&zvqFwII%8E_kS)r=i8Zd zXUsj4Kk|z7b*x>_dp92H^3qo0OglKDM}!!|wKIg=&yblrp%nBOWb{3)(`dQpDZ^z|E4{e||G?=Khn zM_N{71*ff(9Y6uf*%(?6)0PGY!X^e-Vs+~0>RU@{lQFBArh>*WDD*iKcmh`E5;Bpi z7#yZ%FtftL5EM%#|Am;I4i8nQhq|x7O!BKKc6J$&MQ%z(KAA|Ts<)?$_LtnP zAOFT=dPV;_pL$T9^Wl+$FRM&#BvCGGI%>NAyqPOv*pBDBj`d4AUy4YLT z1rdm8IoW zGncYKZLA<8ri|A!-OUEy=x*?dKK<-`c+~s$pEgcjhYx>znB0(m86`K@dJhtoqGgm_ zk0$mC$Vv(Z@Ct{Hd$mqRthwBv(YVYtUfMNIH5~8~$4m!Ux16h*ky7+i@?}ae?x~@OnPe1jce%D9(pA&wK z{vIz-bmIYvzT*AiH_Q1h2DX7Fp*zMSk%Kl`c8XMn$)cMwOSm8>N=~DpSF#gebbMfJ z1inD8ZL|?(s`c>?= z*ZQIa0mve4L^@t4JXIc0r}d0boUGg_4{{Js^MOi8-fX*Hu_IzrZZ|a#CBEPz(i%;u zHRy?+xF*36RYo?p&Q+>W<|Fc(h%fk8!whdjLGQroeI7}8L*z%}0k15DpEk{Z6Tc3G z-&cB@=fZSK73*=^&KAmeK`rZDyPm`&-r4%=DOm&Fk`gsGCf!NPAY0pQ{1xgp>oqcv zCOQhckY>oOx$&(ngXF@#oiP)|_*Q+^PtC(WMfy`q?(@CBxqgAo>AmnSWp5|tF0ifT zQQ=LbNy)X$cuZ}>fryMo)0*^m_--HX2=vg^P1Vg*-x~5fzj8ORhQrcy?^IPf7k0%? zTo>c|K)a6{=HJ=A|L}v{RX*RWiYK!bekVQUn<3fTu<-E#;Je@cWgo8j{_kirZ?%bk zXA`-OsXJo(;gS66F>i&jDNvx^IopuzcdD}2@F3j z^)ulYIiJt^yA=x)UEt#ua+!_~m+BoD7qX@A~=t z-x6b$kZY755-e!v5K zKePFA$4f!b^KHayd+dtI5mra}J{4_T>W8O&arnaB=SAV#80-EY{a#pzS6#z?FR`CD zX#J^$?2@RR9tY5=#ZQ&zFG)3*ce=AeeW0yjbAp7|>KFhzQ5524q{|B+P_Z_#6uPcjontGx4X#(3SLc99_?QkEI<&G+d^yS-BYu zJ!vyoR?K#7v&5XN1v>)=Q`mNSc4pOkZX9VDJ6e048kMdOxU#*&DwSbiXs9nuWLCiK z%!P^aJ4(gp*}i&Yho)X!B!s^m7T-(;4o|s7?;FtnVR;KLl7IEpEqC4rkV76AYA;le z!|dhw=}fCf;zHEojHiWTji{D%xZ6s`A%VB!2}dRk9Sn!K8yI<7xNMnEXLDpq?Ab#& z|X60NQa4m)Bs>G{=l-1NsjEZ7Sv5O%Ot`_ww8bxD8T57notnZ|!7H{YL2Xn(^2zZVz$ z6PoT$s_Z{FBHmaeZ-3%@qlJ8h&DPs8c}4nUp}e~Abf)_Gh?%EbdI2vJtK4>nS`b45 zHn$A17BSlA3bvLCnFW1)Xku$aVQ27|N5=%6t|W51q4+e6!*1sqtJao?7E zYiqi7LX$?!Z72BiOmvHJ1>(%A8g4Cqx&#edGgcnAKm%M)-D&ANW-=}}L&H2QQ;jOq z2^@^)xMXxLI@YT~8LWgacf&aT>{gHN_k=pl#YgyZB6{J<>FDV~sE3^%%sFDIs#8MC z7sF+M3?wy=E7Vtq?J@vhEY4Kak0dxwt9qkNqGTY_yCiFNfu{450NKkmwcIRu+NC{x zwrpcc8a}>>>$!U_QGao15wG^oBF+70jNLm^<4+)u7k*!`JT9lVPJoYOb=>Li?Qnl# z58zp!DW1i2{Cqmo=z&Y4RfI}+`(P|0V{BC*BEKs{XOU*f^thjmhQr1h>WT$oi*Y%| zQe_!Po&ZO|4ChpFjSt2x92yI274E3C5;>K|kx!Fz-eKg08P9AyoqTm+>eJZp3?6WS zDLC0zr$+HdmFdaO^CZ9DSOvQM!emh>`9Lk`^+XsX{%Y*#3opc$3nvBkqBYvBMG@qu%A(_j9lv9U&>up9cMn_c zW2RzvnfFlQ=ZWGoldvbaUxKF^Eft%vTTbI3)SOxJDrsGSU2;gSsQ@6PUEYS%MpA}k zi7WzOORCF|WE5fPHf3iSWT>(Z-f7Kb+u7V)aSnRXezw@WGmtOL_^;469&H2Hr#D6r z+SNUc=XDuaU#6ca9+`grJmWI=V7k5GyHH+A+chvfSQExhY0aeOzMEp}KitlP$$;ZzRE>;0B@H{t=Me=e)`KKrijm3#QNU)LVa z6p!pXf1Z(U`v4LYmJV>_exq;o#o&O|odV~b36Sds-HgNmD(oo~=Nkc&Ie-sk+qU=i zfKGPn)K_ah+cT~}qN3wutFB~uM5bme^i7vUFZP`XJn6A@Qr@cQt|E5IdHi})vOlPw z*!uR`Iq$nin=+ye+v88(iG!645BDBD36ZU{Pw<^exZ`Y^V+^qp^2{6}HQ{tp8*(MCa>Cyos`M^qsjW8)J`}WSXs)e@KXOpDG z(_zJ?BF!o+Nhs-D2p1XoxQdoOUEbkY3ws0!y$^hHm{@w@{bkukQr+)vjGhZkzb=wj zw9jIBb>(bQke`c&*+A{2n`m^RDPAnGaY~;S1jw1~f(g@RYlCnWCC*sdQaXfNWmKjp zOnbOufH}+8gJ?*(32{JKPc$M-D^Q8WlIFv{#xH#q?+(&OZ0Q%OCth5SpRj%ww2&@F zYjK3|O%3cfBd{vBvTWoYFb(q@VrtOFaF*>x%u-ztRxn^{U!N%CjEe*eo`fccDl0g~ zU7oBLkZu<1GDV(EDF?qQ6Mt|3u3FQzGRjk(|w_yE@6KVcJ9U?M(K0!kv$wF2wq@G0w*I z){9CMiF7MWnT|y%v18#K65(YnP8NbS4tMbqkqy4Grdo6;HXyT@&TXH9f$2sHH{wPz zwxnp;g&Z|gGnzfV&6ZIbUU`A_;puB{@%xtEL#x;g;S*zTPWyH%(~WwNEzN4>1ju&1 zawc80+d?xTsv0ub0AS2>wU7%glQ+JTgaa#4OlyShvO*C_dXY0iVBy5vNt3}WsSSQx z7$x#$BluNa@6Ra{zsa7s^K#Nxg;=SLLwQ;SdenH;kdqjs0cD~Ygzg}gG?Nvw`7 zQQl(EP0J)>uq_UZ1+vMZA!4aUH-lA2PsKx_6|~Qcg#@NPoBpA1+VRE9Hs?40)nhms zrxc1SnByA=legIu?;`AerV{?u$EbDm*K+HC}8y@t4SUoR*@M2Og!x?0C( ztohAOa{c%i`yb5&HWN4&K^+BeW*LN^Vh5I#ZxRLkc} zWr?YxIO=wYyfkOvYS9`^>O}Hj0b(69>Kbvlm0H?V@eAu*hc@u)bD;U0`H*kE-4ZY zso_Vqts}uB8;<{fx)AuB5k`QxDz%%i{H&^y0bU4Y4o<{K+ISi_D;Hx+veaOx1&7IC zFW@R%N8`3tDeBN>J5)8}ZqsV?XyZ95G8xnH%)x#4V`J{mtkn0_A@m^zyrO+(#;Yq& zXSzKWIb|^+Z6A@3EEsx(jDwluAd;LITs|D)<4xlpD#L9>jZR&72ztXQXjCmdUM!~e zAz&h`Sn7KZV<8A%Ya_@ccXR3M10>P)C)X*S_V+zmkN8w?=$=^g`|PYZ_R!z0-I}qs zt2)ngMNm;E4T%k{d7ZJrN3A-TZ15&nSK%Bw*b}nKvJM{co6>>f2Ah**x*m@U$W=;X z&NrQWyvQP1J0E>KY)-k~$KQgZcUS>FHez1Lo;h(nc>;Tm6D;h8&_P<-iMqlkRoHS) z7^WFOyC67M{Bp+|<;qDUy3Tmc3NhC)HQdv>i4yO@Y=V^?2CsOKtcKaPmSlTN+4olP zPk58tp{*D9%aczg!PhmtSER2jx}IO!DfdwLtf6eH8&sMSJA0Y2D!#_(IS9i|IsG- z#1d5PSndj?bJAtdVr%FU=o4biJ7eW>P3!#A1G{~(E&d*Va&q5^{Mi1QTs+68kG?x; zxX`CA55OX}U5iv;nzX=e#N|p?t?eP+?dVY4>@;xBY)6$&XEsCwi?(*HZlLq+!pC>R zdAG!@8sf+pBL^%!OA+94&NsQ9rsv<+C!;5c?GGw&v4>REXPRd=o{!Eh=N?LHV5GVv zvD1xWjWkSH$@Y9Hs8YTvb8<|Mi6*h8hFMq`v|q>31Yma+sey8`#U~NALu(}md3OLU zgi2Wp4zb5{U6USn*4)^oCjMbD9ReO}B7UZL7V7b{Z-ekdDTmE^ z(iM$1$A?DO@s&Twx{46Ff{FKq=#OQ{*W}TFQ%me%nZv3yhlHe!LbVgfX5{e;0^o?b zE@vKGr;a5A!#C^w-IjZ45?<0UU$4q1SIHl7sLM@Xg*QF9Mtdex{;tjcx!CtJ&7)AB zkNU>HH`-E<=Sagg3%1xTU{l^`t5M#d75*+>mdR6A+0{i9;CEXw|I@SWCVcw7R`8&7&qOcc zy@Z(2e)3%t!`*HGHlu^UR!E}F!;ajygTU~LMJ^GNup?Fnnxot)1~70`G79Vnu(>QL z4>CC|5DTMYWP%6iWG&dZR)UE@JYFDni%S8Y3wHhvW#ENhUzm6BL*+1k&O5kYDa>F% zk`bdw(*qeE#$98LM$~3HgXgV6K`>zR8Zn>DU@4nLqreun!|brGl}c(i`v_sSQA*MC zigCGAw5(c|Yi_pQ%7L*vy* z-i)>&HJl*WfDMpp>I?`~GeomZJV~(X(Sct}3)f>p)FvQLPS}`Qh~3asboH==c?c9X zV*uIDt-4Nx2Dd(2M}NRo{HkNr`*Iw*$;QQP>QbKT;|QfDWN)M~wO#YFHcd5~Bhle3+u?i#~)#^m}N zwMTalZr|4boCA{fwP62k`Ohbrub0ruD}wJOd0qU}@65N}>*hZ`Lr)f)tzB3+Dm#N6@6DvyQlg@D)ViyJZ30;?L1(9f zSzgXNAi~S(X2dx=k!0eCLgy?xQ(9v1&D6Kf2P9*DQZudKbf zbcwlt&fk%2G{|-e+e0J6(OS}_;V1%~y$!8&GBwz2vne9g(RqKK#yK_#C!RB1Rl~8KA=O&wU7Pq{~MKHScvIBr^ zvIyp5shN2+{>=@h_h7viecT(S#ABR1lfCltvb3J5_2^PJnRY9grs>rhXi>5oA|sHt zz*aTLbVRkRk_-Y&(=5$RHFyBi#j*&BX`T_`k~`#WE-z!q-m`czN=eTRmPuJ2Gp$z=$eh&a5zhQv!`BsBY6aM7L?#(6l<; zXeE7ETcBDZLwF13a?}meWT{hou;Qjd+a|+7l?EV})1v(lZ$64)KUNg-LiL$5ml$@> zVvp$-DQ#}E*he%hk6EUxG?G@k@kDOujDmI2Dfqh4rIPbT%9OTc3re8lzyQZv9N1a3 zs!B6MDymsC-3Mbz$=2dl{-(>Cd`}g4bYtHiKTYqbbp&=^_Ll+9F14(e8rTeHVrDxK!AYeh+2gjHFPMr^=7>9oSoIDYtdAJgSu!o916CY=14DS6{t; zfuA#tZ|EM!!_8S=<9K{%(h`=OO}8mk(^-ZcOWQWN1WaHaZCy5B_>`%}m2#+FSckgU4AL%rZrVMiL8{(`7*W|2~S!r6&D`A73g+PxB;7?;4n=I;F3N zubye1XYKju=}hNy2`vtbdJxRqYCIxRWKWaF>8I0FkxvzOqz6>$My`N|=}M6SrCLTB zp2LIA7|HPp*pP-8j=;F%RB0+r6_Cq_HqjN$sgqqoX=#eLd?J>j%I)@1hhS?-=Hl&g zV>=-!oBNzB;>}8fnmCnKbHxr!rZEmY?v~Mp$R%yX)ExnZmcRULBK@8f|L+Sge{=PC z!xfV1xbyk7Z&iADBd~sYoK+cJ*ZtSaU){7JfA>+zapXKB-ukuu;p1N4dVZ{<7v_2$|+x3mYt9QHu-1ke=h@Q~NmzR1I92E)twX5(|~ zW;D+epSI_2M!H=x8xK}uPOOXd0lY0k(nQ+Kw(h{#jdmpiY-B#*OniW@c_FWS;F~^) z-VUCRo3%iH!SDms0>AsTTzgqu2=(yq2#UQLAF|ZKnQ&IKG9+)o17QTKC>k%4@X+n& z4!SVAWD<)eujC^JFjwU+#(X>m*3JaBhCWz!sSvZc7C<=J^6ldRA^(I5a~u;FZaurF zp*Q~NSH7Kh06ks+X3pTYd3{2(jZWs}4F(R>6X@dxmHJe|bUpns+c(^MdT#U|H&14B zZ!3E&ukMEK&G0#%_06bT<~v=5=LV2Z$0;|!$ElQ+!viCy3jjEPkQy&w-yXX73L78? zJDiOIRgp)N)aoFfn+ycTq1BegWpE2ZOA*;F;hcUvXmWi6Cw~U>#{M?@?aiZpuDJV_ z@QqMTr+v%2GtC_eD;TvVSQhBRDN0zNE(a~R01VXGY{}tlG4v<#p28YEDEu)x%QnPaNIfvj=VW4+I(h>|As&)%NR(^&8s1IeWcw&bIBLXshs6 z!50k@5A?Y^ky`-FrOf5R8W}3PQMk+1u@xIF?F$h^Or(rc(Rn1!b!mezhOMkIW>c-r zZKI7kf2Z&FKBy7*cI*dCq5ItZus#)kNb`AHpNjWWdZ;xa*ORf#ErAR^P_$A62MM1{ zX((IA1~EBw+dOd%0vaocLu;k1aV?8_tyzeIxJ`z{E zp?hNK@3S*QdqmcncmnTeAOxh&ZE?hV^hb z%vd&DL*jul+c<_TOp7MR9f{qCySjzvYihDyaI+~X^LU5@LuTu@8)h*D`PyDeE6^}i zSDviaro(0dG1hzJl!^01Yb&0*%)YCj#-w~ zg+iC5y@-?da4 zZ6l=jtF!P&w(l3RCr1A6&~>KvIXAa^S5K=*35~fIP;R#&w7|qPb-O`v2K0rBlvp)8 z4xwg+S>3<`SK%JF<7u|~?t5xQeBiS-3rCDpTg}b;pQe)m&`+Q@RBiwCBk^a1Sr*1Kzx9^L|g|{CfENhVF?wzt7I9 zW{)tq8U{-3@DaX_3Z-034?vZb!b~FVj4=*d7u#fJ_->_A zE^cRphT8(l(4K~mreRw#yfLT;5-2||A=A_I8%qFs#h72$j%+U>BK&WcTjN_Fi0u(H z(_6y7rFK3&=SB2bbsC`+*J8S#f(4)AFr7;~i!Ju^sZ!|aaT8eh9_Lj!OHAeEV7TWg zeB0Xf3@FuVndPxd?g!>U*eI>CZE9(g`~I#QJnmexqh#;jNR52E9nXo& zY2wGfQPbXOy7x-N4?w*1l<>=g?iHjxpgX;geU|R!1q4?+2bjiEv5Dk?pV*D$is6u$a<#ip)@-wqyh4cp-pUUYf?41{;4QtpsNl7)Ck``lZ@w%dX1O@u*&h z!=kK~Tx|+^d;gBJrPxE1NeL>kDh(ai3S8c_ zS2n)p76GJ;NFb_KI&MmPdyUAX02A4eqM&?3BWoxnxEk!BT{|+#0+;KWobavU;3PC3 zmEYY=eu00X|3c9E+&9ZUm|ykeBYh@wMW}E)agXw>Plp&+ zAVFYw&1^U_2re(8Y7a=!my1Qpre70F8y(_ukJNi>Vg0s zw7ftj(P6D+vq3vC0p`GLWzCy}>fBpLHPQ_stbsWOD&qx|L`PK?-IMZ`T@E~B-L(^3 zLy8em6Rb2a?c|#(L7(0&$2Y#j)^a^|?<2)LV!^#8ec|rSg>$k+A7u+$5q^iOSWD-S z*_duE9t?wTh;4wl%NDbEtK6aaq_$_7ms`^nF!Kl80WVttT+jvpv*R|l$MdPrX8C9` zmKFh7JnmXK8sTvj886V<5AEK)T(zf6&L_(*FMYr78FOFHXS<<$63XwhzWBGGlMf=E zgRF1~9EeS5&Rf9XCz-G+ngIcpa%*!6K@x5v?!73}cQyo8&8*s2hA_;AGZ$xkFM))u zFwr`2M+Ah6ed;&)iLriqRmU#wM$p>L_SeZmk09 zJuJ@$JbVANF>!i|=|5e2ecs6`A3iYbZz&%ob$j}BA^EwmY8w_-xHFi>#)@Lv#9+C~ z&c{O+Vl*}$Fk}qPluffZjOot8c_-U6ThUyoE?q{Eh)=?GK8Czp%Bgs~TP49dpK$vx z?~GM??g4G&NpsYsi&N)>C(F32H9V4o|NbLRY`ltf(10om1!*(Ww zeAjfMRL`f|?ZRC(Q=0Y&cZ(lr`7j1`FIiIc0@iqv<#1;4X;;FJN)B`1a_zl=$`1nfK+bL*IBZlOuC=_nOWA1_{ng;^Wul)0<%VqjAbpQ;I$# zq0z(peU_nzl(q}WqvS6KXH%}nhE)~=k|smUAOa1nLuF<+%V46;qm?x?Wf^6or8sL3 zl~gqRv}kOb*_ESW&JY2#D7p^VXq1?Trxqp4I7fzL3r+3lapn28i^#QQNDp>;{s)i& zkHmb>6i>`NefCXEre-{{389UGtm=>eL!`|R4M%-IZ_-B13YOB8AjsxrY^ZKNp8)Ni z$UOxNDtdD0rn;uGds@K~S5Jk-oId{B`^Mw5$1m)I{n2p#W3fDgA77byPRhNx^3~b! ztB*J11Xub|Xpnfwbbq0GlHm3DiRfpjYck94;XsdO(&o_Mi(5XJb ztKVY=h60VAgl6WzCFK^HHeFriTD<=Lvfpf%o3MuD7r%2qc40yl~Ta5Zw2b#vj)5G6tQnRJ!)q z)|dJ1J2yM^mj}oGc{CWh8|%I2!{Of5_WDA$x08$(c1CZn-0X05|7?G+yS}rxoowG6 z?%!S3&$5#(@3`LUiTcH!+waaEZEYQ%pB%eM_~Bg?!5~ZEU!VJ7ImcVbPbtWsSO9WV6u}r>^H-a% zww4AbJ4Xl2)!WT0e|=WYU&V`Q_BQno@9;UjEB?*#^H#L6wfEJ5KQps5?0@y@s6MmP zU)no8o~u8fKVIo9E?n$)e|}FLcmR3k`vQM{RbmAbQONHeTMc&GryGav`N8Jw;$Z*v z=|!}((G<6lcX-?m+UxDs$xZ83ytMmVHqTa<2FHtga(iK8xVpQOtZW`!%k`7_ertQ~ zWIsOLi<-mE{89Y+uKnK4iz7Gb-P_YSUz`5`H9;T=AMpDH19^l#@Q>fVJvw~;d}TY@ z+p1qD+tJ0*+5D^R?Ss?TyKDWqhK$|K?cU-bIfvkX{k*>(-5uO*gwN*|z17BkHrHBd zwDwL9R-(a{m)v}{lI{Ksr5}g`Gd7G1<>+86A}xH{6X^^1Jj5>CAk00jS^K6*2fO)k z;*ED0@n-R%4N4twYfxbd#y*|M<(zY~LEzup!av5K+=lv*lTfOIl z=B5`eZ)I1TZ{^)(`|Q=((({ee%xl(nmVee<>7&qEcm($3-1CQ|=`6U2)BLX&GmlHi zC1=<2Mn<6BU)AIOhBtQy0sm-kLG;DSoeXw%7Y29PT0N}~_Rqe0C2kM0{l*~J8J=t& z21mVN=Gtusf6vd&((joj|4pyCnU>RN&&|y>TC=UjoHNsCw`UtqoSBccAp4cc)JohDw7jGWy zyfD|L2Vu9>sW_E#WpN50q^9)Wl%I@}j3BPAEuU`u5cQygs?L##S2xQ5u5?6dUEpQa zy*TOyJ>pr^UZf8v`#Mv8oQmj%gOj;h+xQSueV6tXp_77{Q%^tGBlke2ir>`6>38#? z{<0BHEwt=~+)T`*GX`GrBjn~e8v2E++M~6l)!nt)!2gJIyhHw5&Grn*e`~JUY|qYu z{I}Ze_9yxOI$lqmV=Y|ABwUrsl7o$iAl^AzxrU{n(632~v2fZl>6MG^`!;vE*w-X8{P8<+2dP{(eJUenEQ!%JjDl1u%@R%T? zmOAOU&K_ul+N)#79S)te@1~CDMovdKpgSBFZ}1XW0SrrU7);{>NLaKKDSWtbu(j_D z;W}LD2fcnhjBlNw3y^@$&aEpei3^|N4Sj$qK%b?~(3L5!mp%fY3A`&O8#*$Mqnfjo zIsr^8Wi}8#uL$BeOwD- zeC8nDQJg4(w8#vP_FARg80Ckt{V-{3tPa2b~o;&XLV zN7*pI?%3|&XWw~qi*P2m%tkHLOXKS1>l1m3x*y+0VeIAcD4{t3{W7k>FNO`tXez;F^8J_KGgFdgTOH+key#zLSFi5x^UhgHbT z2Z8E4wYvJSCSeS6;tE;eEFo50i)5CNcspEtcHY$AVY=UFQ6+Y#udzWAWfJ+2ufdfR zbOAAG7(;x7e}NnS)BP z15W|fEZ!3a*TU(HDug^lz#AJ=OrbY`A*T+?pbjhpz|Y7cXC$mb{! zS_w(QAnYJW^rTy>k`>Ac>RMJ6=G) z@GiPmxlo%WB!vu*#>9XLpft^suZbu<1`g*5-7ErA1fTq(?c@LVSMAH4t(CRC3go4Eleq`A`1e*Yo=3cYou_=TDyOx}LLt?7Y@lB)sre_6Wa8+`qV`{W6jgh35Fyin8zVB|slxi7DG zSGU09*%qT=KLHa-rUv6FA!+z${|G<+=E;-af5g}w{6ZtC*5kpC=kV_lm@|~pg6%FhN9<&=n@C%=*us@N8Wh=h6r{d(JI8~{mL)D`0gNn8-Ne!46_b|@{S7U zzx?8N%n&eu%5D1ci|>GQ1C{{0ioh<&xSOIU`K@pN?(Clbjurl?Uf~zN{jJiipOtQ{ z=v$9O^sak<>&d6r&*638-fcqMB9i*YvjgwY|IOJ(tEB&D!6rO$?mog=#$LbT`oA^f z4DjoUf7qCBHd}KGV2f+>vp|T&*><}EWN>!2mY0rJHn+~!YIkm$Chw;Me|QlsOZWBO z%z*Et?5zpG$|UOFg}b zWovKClXkh%gKf4SyUnp_-$S`pnhP@@v_-a{)(%&~rNgb&<)t&PvvC$(oZt2?&u3@c z*GIFj@0MO)G#4{}^VRSoI-7CN7c+0Pd~+Er-}oE-(2MrEYs;5ur!^dQHtzaco6C)h z!QJp;1hCKd!cMdo0*n(IMyIva^9N@muNmHS!iB^2^Tk#t+U>zT&CA!X!poKAx1HwY zjNe=zT^{!KH{PD@UmEy*_b3G`?rt*-!D`*YVm_is*n9 z(GDx3s!5nOYcrJgT@~FR3P_RR-*9DWC0A8b6>cIJ2p1HUaJ{~hf2Ri~iEYKGQu&F% zWLniR8c#@-x@1^%1Lth3lo&@HxT196vC@GxTuigL7)RO0;-WBp2P&fzL>ekxTA8e( zoo7K8^K{dgPxPJ6$Z;8%JrC#9Tr3(SY2am{o7kU!UlyR#Qd{;?$JSWh9i{Q^fllDs z1g6K&;&fFH5PmL-1kAG6V4CiK~b%hP4~}K{w!ZuY6ztOh&BaagrR%LYP3r=VcHd z5-_Uf#9cziETCg60xAyyHm-;gx5oll%y*NK=5F2cLKsrma%)V=$6`ez#21(;?+7fS zm=+w3qGg?nFVpA;z|d;qjy3TZ{cl}NEMo%v#tlM3au!jM3+_%uGC=g1X|>FPc%r}v zHwgogXihhxjcI9Ez2k;5eh8d9jyb~e9ALRb^&-zs^suIn7>YdhQvflbfo}E!pa2pEh2*Ox2!VMKoU+S z>X!vvnre3_=@b~|=H;yW0pJ7Yc(G7Di9_)J&0-En=N>?DCi>IMdn~&ElDrH;{cF!%EKau%ffow!%IVWZlE?fku@4<2oGE$*GlLH13AS? z+tM9sRtsN49^m1CXL7n~DK}j$34=k9(&nl#5U%+e8N(z7e^yqRqC?{8OfoRZ{GLyd z^CA)#^{j$o>o(f!ZJdQZ)_H=cizPkbv!>|AtR56=6?KUMMtpYP`#;O9Uj)K`P6(x^yu&{7f123dR)JRFN18 zIF$?cPMr-Ai3G%`>As2J~v_h&2tiX6@B5^OCx_|a{E>;q7qb*9m225#vGLj*L46Tud^ z8*~9#L!biA@oaYH_ofSm!2uP7O>v}>WZ%Ue5`fqNjs<2wOH%-Btq@qG0+Of5=WL+X z;Oa97s;>hGJPDvaQ=GGgj$Xmx0%Z%E>@fhWERz!u`gmLbHkiaCH^lEVO@!7%Q5^9D zxnyu-gu=2#OmFT`0%Rj^swWzaG#@^*+!UokSs(dg;3n78#vqL=)}l&{F>ruY3Yfvq zJa~(Td~-i8H*F+gBV-gg3|)8w3+j^pi;amK8L_fA5;}1ERYnRL24xjc z)tE_*X%I!jP};n)4!LQVjD&|xL=)r2HCO~atHN1@O{k5X^mCQU3`p)hy2Ghb=8QiO ziy;cP>glj2S11De174aivvkQXx3NPa&+RGHHd6>n$w^IQ%FuFtkR=rB9ko#MwBsNV zE(w`)>;apH0nRjH;}unbX1V| zB&DP8%c3^1W9#z7Y#M@KdQwK@eo?YwE1iFKo`JFz!_-z5C2=#rl-NyM+x8}_5959! z4aR7$nl)XBZX!?E(dw9kSTp>dGYAB0f&)EvXAzrSfim!SbIB4Y@Mz#e~{| z%p+EFd<@(XvUw!XRY(65O@0ha&;3v)b)nM;s#B9`Z>gGAtvyYW4NljwiRnxtqoNtU zunttU(iTKzM5$$#!!2As@s!t(xzo3-VkRdfm}0IKU)rsexre0jP3@q1GZ0EPrxAnW zNry?v2F$@RG_Z)E)QsjSj?7WjVpPr6oFhSxQ;k>=-sfT@`YvgwVDis90(8_3pwfs% zQ6`8XNx=m@d@!|1Swc+5wukh9%)00_A+8j!X>2nSnuGx5KgR=|2g36vCS%s!AaPEm zn8fcYt${udV9v)0D6+IU|3bGgTWTE(E1*Tty#doP;(Uc4dw5`zz2Pp-O6Hv2`NCDf zUczdE;UHVzfLR9|({kr)1wWAPQ3RAgK?ssswI=>(vDF9uCp3FzkPcZUn6Lsk)b{9% zwvZd>;9JlMLJA=8BBrzp)~2g-02nT*l`Vm>@=JG3Qc06impY&aL{G|k62noyB;~;! zc9J+VOP5Vn96ZNNrl?H{X4jH{q7BB;Gy*g!hIkNH1jC@n<-B0c(|(@1ohbBZ7x7x8 zd9XBhLFAck{rDE|8&NxG+`%jgw(A2c-LqiOm%ykx!Ig@^klm^fBVLkKB!idgUVXKt z@0ZlKMG1zb@{28W_R0h=B{3Dzw@5SEns5@y%V}?zJhJ=)<(`$s5m_kh+7acv?j$y{ z-Elk7Z0WoPeM)dsTv8A3jqge za2j95xbY_N^&t=&zk00Bo0XShJP?b^StX-ON$hTedlah|H(9U{EsP1uj9&=p6gD*~ zl|DkIvanhhp=Z*lv>XY=vPJYz@Qz$>c|eM+me*6#3wdNXNhj4%Oeqs-Ei0fSK}C;N z4OFI~P$^JF1zWYnk>K2NA|{lLs=A9_E6#B}iG+Pj1*$s1S7I-R?Qv=cQsFp*m*-|k z6WE*uxI8~4on@RG1jA;=r7k0PfT;<-8_az?^GLMK4Ez#0c#PSWYAu9 z0Zp)UV(T_Wo9H{s5(6U%IP@w=6uGQ)&QrVYT2dnghM+vlEqCtts5`1UU^IWjwkhw9 z6=Q%3yF_PXl&4ZcI~wo8pDsBeqiJ1R02(4Q-Y3(x@*a=mRJ>t?47bl4p0K|{aa~oI zlmL=nh;rzUoa-pQjliel{1`}*=VghD2U!X2;9M`UZ>S|~DUUNwkgR2W(vRtUP0R@& zB}1_`2d>iZKDMcm!8YF4FN$ zQfA@3PP)%)l)Oel_*LesWIY~?t+E@f*G(R);nbH@wkX?YyCtBYq93kTlaof}2ko?K z#bXmk*;1WC0}s@tG$|%JfW3GqnHduR4MW#s2bhv`Da@-YxWp*FLUAcr=)5`ml-a-0&@<$bHq^8jej={&2dwYdfRi{t{m%XUv*W5zV4h=H8Q zRP}67b`!V`xE6FWYF_2_;NKA4p5$4DLfW=SNXo~iYB0>eNB;soxfhUY$ASVues~Wn zqt8N|V(C;Ks`KA8D8Nn!<6A^_+T(!*a#z4TJy0s#B8^X{QxOO=N7cg$vOU8W&9_+h@pyV4RLWjB(2SwPBL)cPKw#A zR9kenhbHy8Fg!ECN zb;+7on)v#j6t&8bFf1(CmBUNfcWu>2glVDqXPtkeba9$1)*@u-%yOF@&{LO`5TZaO zV;V!W4^EyJ$GJ9nw2{IM;Vv;9jmPf+>zoQCQSJz#Hge;Jfgg_SG8KuF;=2O83CdLS ztkS~P))*TyOKz}AX?+5CnWF@Pa<~vD=gQwA*Jn}`Y{LHuYhZmb<)SE0DMhJ$9&CIv z&YW_ruYr?HH5)W^f^9c`RaX!>Wkbdon4BP)2(CJ@n@UX(h(gwXd=uDMP+|G?1v(}Y zJFyaoTOvy5K=2!+%6wb;Qgw_tr1GK{iuvIm6(`Xuqfn#C3gk69&dSU93c{^G`LL=3 zKnJUnR!AF_R=3J=pBlI(b*d3PgxUqtSM`V~50j9FZ1jxMQpI3qT0QQ>r0O(X^3oQU z$q?TaQoutuU}w{d6V=pGo@ngM}DqDPO+@x*l1wDshr6z=F@5iOBNw}xT#d)p_$wz~wz5QC*!Q%qA@ zt0O2%V&4+^z={BM$5P(AL}3&+)_QohCdKze>_}AvG2-yuf!h}HY2;8o#X z*%T6RA`5-lY2pI5k{?XJcX#)hb|dTPeb^ihiMc zYXTl=e4~7|pl{F}vcqTWgjRj!A=eZf)r_T^IjpTJJ*^n%Uj9)@N594BAE?fHFlmF$fJTVHrpm6#k?!HhI<}gW8GPSSNQMnn2mjITRoFdmSl9edwP=BxOH>wn9 z(Y*w!EJEl3dKTOqHxhBi>B2(Ix=@x^F1JW69}4&30WQpQud)t4J_-R1u&Tn%gK?MK zZ1OqvKF7R3c-t4D&jB_9$RdG4>n0q#-ZWyVa>?<~!5-X;wr=G|5n{s{n!+G2yVpC==p0!Ka!ioaa$Rte6m_ zbl|9hhXKwC?Z>7UoIY#-ABkIYi|z{6hs@T28GMtsw5s@^zy;VR-=tn#%ZbeEoeCUG z)S~_^%K7FEn-c(114iM*1-Dz~y^eGOSN7>NK(VvBkP*V(5#8yco7j2*S_Rc7Ms^oC zG6P`9nEGb^R>T^i$21(`1d|Xop`Q6bj8n=X3O%O+R-}&_lXn>Ct`%(W(?Rp3$*jKo zNqsxfXi_tf!fM_y5a!uUv`32Egq2oJ^+v$mWrS>X`3YKFlr)`Z)WKC~Nyv%CAuNu% zrS2LP?U=$QQQEH3l8j4JR2hvILebaMWHyborM{vMG_Kg?V%S-?m~s9{7!9Fqyjh(d$dzlU8!hv7PBZe`c6AtgB_|$)fng6+HPr%i zZBr*V^2(3J&iBFB^nOI`^uB7oMG*zfKB{6RVT!#J`C+{>B~{{96HA@`1H73dkj{(o z4e8Nboz#p>7H;g`Kx*SvsLmCo`raeDs@)ZjrLd{98f;+L!491tG;K8&8GI`Xh6!{o zf_bh{9oz?k`ql6kJ{<97y{?{IK+jo+De5qJ9Z+H_ASjxq20~t{3=$n(fNO5kfqF%( z*qN46$qN%!38*~EJTCAV@jVrw)LC|OsLQS36Bch!3z%mhPAf0XPPv<-Pxuf%!Tipk!YGOO>~9hO`rP4i+a5FxK0o5Kzh+P2GGS%qq7Ob%V$% k9KM6wh|em&oR2y9^!oJr^!oJr^eVmn7gK;QQ2^Qk0GGjk&Hw-a From 9b734f47d9a6ac3a452582736807805d27be45e5 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:19:47 -0400 Subject: [PATCH 04/13] added npm start; use humidity in forecast --- README.md | 2 +- package.json | 2 +- src/services/rules.js | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 31e71ea..04868a9 100644 --- a/README.md +++ b/README.md @@ -36,5 +36,5 @@ npm test You can test locally without setting GPIO pins using: ``` npm ci -npm run launch +npm start ``` \ No newline at end of file diff --git a/package.json b/package.json index 90d00dd..bc988e4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "scripts": { "test": "jest", - "dryrun": "DEBUG=true node src/sprinkler.js etc/sprinklerswitch/config.json" + "start": "DEBUG=true node src/sprinkler.js etc/sprinklerswitch/config.json" }, "dependencies": { "dotenv": "^16.4.5", diff --git a/src/services/rules.js b/src/services/rules.js index 416af72..db69d99 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -4,11 +4,15 @@ class RulesService { precipitationRateThreshold = configRepository.get("precipitationRateThreshold", 5.0); precipitableWaterThreshold = configRepository.get("precipitableWaterThreshold", 50.0); cloudWaterThreshold = configRepository.get("cloudWaterThreshold", 1.0); + humidityChangePct = configRepository.get("humidityChangePct", 1.3); evaluate(facts) { return !( this.exceedsPrecipitationThreshold(facts.priorAccumulation, facts.forecastAccumulation) && - this.exceedsWaterThreshold(facts.maxPrecipitable, facts.maxCloudWater) + ( + this.exceedsWaterThreshold(facts.maxPrecipitable, facts.maxCloudWater) || + this.humiditySpike(facts.priorRelativeHumidity, facts.forecastRelativeHumidity) + ) ); } @@ -19,6 +23,10 @@ class RulesService { exceedsPrecipitationThreshold(priorAccumulation, forecastAccumulation) { return (priorAccumulation + forecastAccumulation) > this.precipitationRateThreshold; } + + humiditySpike(priorRelativeHumidity, forecastRelativeHumidity) { + return (forecastRelativeHumidity / priorRelativeHumidity) > this.humidityChangePct; + } } module.exports = new RulesService(); \ No newline at end of file From ecaf81b864c51479432eed9627339e9e418d6cca Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:28:18 -0400 Subject: [PATCH 05/13] Concurrent fetch of remote data --- src/repositories/gfs.js | 10 ++++++---- src/services/metrics.js | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/repositories/gfs.js b/src/repositories/gfs.js index d8519e0..1f28da2 100644 --- a/src/repositories/gfs.js +++ b/src/repositories/gfs.js @@ -29,11 +29,13 @@ class GfsRepository { } async getWindSpeed(lat, lon) { - const windU = await this.getAggregateMetric(lat, lon, 'ugrdprs'); - const windV = await this.getAggregateMetric(lat, lon, 'vgrdprs'); + const data = await Promise.all([ + this.getAggregateMetric(lat, lon, 'ugrdprs'), + this.getAggregateMetric(lat, lon, 'vgrdprs'), + ]); - const windUByTime = windU.reduce((acc, result) => acc.set(result.time.toISOString(), result), new Map()); - const windSpeed = windV.map((resultV) => { + const windUByTime = data[0].reduce((acc, result) => acc.set(result.time.toISOString(), result), new Map()); + const windSpeed = data[1].map((resultV) => { const resultU = windUByTime.get(resultV.time.toISOString()); return { ...resultV, diff --git a/src/services/metrics.js b/src/services/metrics.js index 915a5ad..4648eb2 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -7,30 +7,39 @@ class MetricsService { const latitude = configRepository.get("latitude"); const longitude = configRepository.get("longitude"); - const precipitationRate = await gfsRepository.getPrecipitationRate(latitude, longitude) || []; + const data = await Promise.all([ // Fetch data all at once + gfsRepository.getPrecipitationRate(latitude, longitude), + gfsRepository.getPrecipitableWater(latitude, longitude), + gfsRepository.getCloudWater(latitude, longitude), + gfsRepository.getRelativeHumidity(latitude, longitude), + gfsRepository.getGroundTemperature(latitude, longitude), + gfsRepository.getWindSpeed(latitude, longitude), + ]); + + const precipitationRate = data[0] || []; const priorAccumulation = precipitationRate.reduce((acc, result) => result.time <= now ? acc + (result.value * result.duration) : acc, 0); const forecastAccumulation = precipitationRate.reduce((acc, result) => result.time > now ? acc + (result.value * result.duration) : acc, 0); if(precipitationRate[0]) console.info(`Total surface precipitation (kg/m^2) @[${precipitationRate[0].latitude}, ${precipitationRate[0].longitude}]: ${priorAccumulation} => ${forecastAccumulation}`); - const precipitableWater = await gfsRepository.getPrecipitableWater(latitude, longitude) || []; + const precipitableWater = data[1] || []; const maxPrecipitable = precipitableWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); if(precipitableWater[0]) console.info(`Maximum precipitable water (kg/m^2) @[${precipitableWater[0].latitude}, ${precipitableWater[0].longitude}]: ${maxPrecipitable}`); - const cloudWater = await gfsRepository.getCloudWater(latitude, longitude) || []; + const cloudWater = data[2] || []; const maxCloudWater = cloudWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); if(cloudWater[0]) console.info(`Maximum cloud water (kg/m^2) @[${cloudWater[0].latitude}, ${cloudWater[0].longitude}]: ${maxCloudWater}`); - const relativeHumidity = await gfsRepository.getRelativeHumidity(latitude, longitude) || []; + const relativeHumidity = data[3] || []; const priorRelativeHumidity = relativeHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); const forecastRelativeHumidity = relativeHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); if(relativeHumidity[0]) console.info(`Least relative humidity (%) @[${relativeHumidity[0].latitude}, ${relativeHumidity[0].longitude}]: ${priorRelativeHumidity} => ${forecastRelativeHumidity}`); - const groundTemp = await gfsRepository.getGroundTemperature(latitude, longitude) || []; + const groundTemp = data[4] || []; const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, 0); const forecastGroundTemp = groundTemp.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); if(groundTemp[0]) console.info(`Maximum temperature (k) @[${groundTemp[0].latitude}, ${groundTemp[0].longitude}]: ${priorGroundTemp} => ${forecastGroundTemp}`); - const windSpeed = await gfsRepository.getWindSpeed(latitude, longitude) || []; + const windSpeed = data[5] || []; const futureWindSpeed = windSpeed.filter(result => result.time > now); const avgWindSpeed = futureWindSpeed.reduce((acc, result, i) => ((acc * i) + result.value) / (i+1), 0); if(windSpeed[0]) console.info(`Average wind speed (m/s) @[${windSpeed[0].latitude}, ${windSpeed[0].longitude}]: ${avgWindSpeed}`); From da82a06255f4f2faf5cae32e06e82fd45ef0db69 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:54:18 -0400 Subject: [PATCH 06/13] Switch to specific humidity --- __tests__/repositories/gfs.test.js | 1 - __tests__/services/metrics.test.js | 6 +++--- __tests__/services/rules.test.js | 32 ------------------------------ src/repositories/gfs.js | 4 ++++ src/services/metrics.js | 14 ++++++------- src/services/rules.js | 14 +------------ 6 files changed, 15 insertions(+), 56 deletions(-) diff --git a/__tests__/repositories/gfs.test.js b/__tests__/repositories/gfs.test.js index 155a674..0763ef4 100644 --- a/__tests__/repositories/gfs.test.js +++ b/__tests__/repositories/gfs.test.js @@ -33,7 +33,6 @@ describe("Obtain NOAA GFS data", () => { }); const results = await gfsRepository.getWindSpeed(47.6205099, -122.3518523); - console.log(results); expect(results).toHaveLength(gfsRepository.sampleCount); expect(results[10].value).toBe(Math.sqrt(Math.pow(0.000030000001, 2) + Math.pow(0.000030000001, 2))); }); diff --git a/__tests__/services/metrics.test.js b/__tests__/services/metrics.test.js index e91a8cd..713efe9 100644 --- a/__tests__/services/metrics.test.js +++ b/__tests__/services/metrics.test.js @@ -7,7 +7,7 @@ jest.mock("../../src/repositories/config.js"); gfsRepository.getPrecipitationRate.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); gfsRepository.getPrecipitableWater.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); gfsRepository.getCloudWater.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); -gfsRepository.getRelativeHumidity.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); +gfsRepository.getSpecificHumidity.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); gfsRepository.getGroundTemperature.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); gfsRepository.getWindSpeed.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); @@ -45,8 +45,8 @@ describe("Get forecast metrics", () => { test("Humidity", async () => { jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); const result = await metricsService.fetch(); - expect(result.priorRelativeHumidity).toBe(0); - expect(result.forecastRelativeHumidity).toBe(0); + expect(result.priorSpecificHumidity).toBe(0); + expect(result.forecastSpecificHumidity).toBe(0); }); test("Ground temperature", async () => { diff --git a/__tests__/services/rules.test.js b/__tests__/services/rules.test.js index 9e0d5dd..abca32e 100644 --- a/__tests__/services/rules.test.js +++ b/__tests__/services/rules.test.js @@ -56,35 +56,3 @@ describe("Precipitation rate", () => { expect(result).toBe(true); }); }); - -describe("Precipitable water", () => { - test("High water volume", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: 51.0, - maxCloudWater: 0.0 - }); - expect(result).toBe(false); - }); - - test("No water volume", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: 0.0, - maxCloudWater: 0.0 - }); - expect(result).toBe(true); - }); - - test("Heavy clouds", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: 0.0, - maxCloudWater: 1.5 - }); - expect(result).toBe(false); - }); -}); diff --git a/src/repositories/gfs.js b/src/repositories/gfs.js index 1f28da2..9b51d9c 100644 --- a/src/repositories/gfs.js +++ b/src/repositories/gfs.js @@ -24,6 +24,10 @@ class GfsRepository { return await this.getAggregateMetric(lat, lon, 'rhprs'); } + async getSpecificHumidity(lat, lon) { + return await this.getAggregateMetric(lat, lon, 'spfhprs'); + } + async getGroundTemperature(lat, lon) { return await this.getAggregateMetric(lat, lon, 'tmpprs'); } diff --git a/src/services/metrics.js b/src/services/metrics.js index 4648eb2..7c33125 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -11,7 +11,7 @@ class MetricsService { gfsRepository.getPrecipitationRate(latitude, longitude), gfsRepository.getPrecipitableWater(latitude, longitude), gfsRepository.getCloudWater(latitude, longitude), - gfsRepository.getRelativeHumidity(latitude, longitude), + gfsRepository.getSpecificHumidity(latitude, longitude), gfsRepository.getGroundTemperature(latitude, longitude), gfsRepository.getWindSpeed(latitude, longitude), ]); @@ -29,10 +29,10 @@ class MetricsService { const maxCloudWater = cloudWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); if(cloudWater[0]) console.info(`Maximum cloud water (kg/m^2) @[${cloudWater[0].latitude}, ${cloudWater[0].longitude}]: ${maxCloudWater}`); - const relativeHumidity = data[3] || []; - const priorRelativeHumidity = relativeHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - const forecastRelativeHumidity = relativeHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - if(relativeHumidity[0]) console.info(`Least relative humidity (%) @[${relativeHumidity[0].latitude}, ${relativeHumidity[0].longitude}]: ${priorRelativeHumidity} => ${forecastRelativeHumidity}`); + const specificHumidity = data[3] || []; + const priorSpecificHumidity = specificHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); + const forecastSpecificHumidity = specificHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); + if(specificHumidity[0]) console.info(`Least relative humidity (%) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity}`); const groundTemp = data[4] || []; const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, 0); @@ -49,8 +49,8 @@ class MetricsService { forecastAccumulation: forecastAccumulation, maxPrecipitable: maxPrecipitable, maxCloudWater: maxCloudWater, - priorRelativeHumidity: priorRelativeHumidity, - forecastRelativeHumidity: forecastRelativeHumidity, + priorSpecificHumidity: priorSpecificHumidity, + forecastSpecificHumidity: forecastSpecificHumidity, priorGroundTemp: priorGroundTemp, forecastGroundTemp: forecastGroundTemp, windSpeed: avgWindSpeed diff --git a/src/services/rules.js b/src/services/rules.js index db69d99..450e213 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -8,25 +8,13 @@ class RulesService { evaluate(facts) { return !( - this.exceedsPrecipitationThreshold(facts.priorAccumulation, facts.forecastAccumulation) && - ( - this.exceedsWaterThreshold(facts.maxPrecipitable, facts.maxCloudWater) || - this.humiditySpike(facts.priorRelativeHumidity, facts.forecastRelativeHumidity) - ) + this.exceedsPrecipitationThreshold(facts.priorAccumulation, facts.forecastAccumulation) ); } - exceedsWaterThreshold(preciptable, cloudWater) { - return (preciptable > this.precipitableWaterThreshold) || (cloudWater > this.cloudWaterThreshold); - } - exceedsPrecipitationThreshold(priorAccumulation, forecastAccumulation) { return (priorAccumulation + forecastAccumulation) > this.precipitationRateThreshold; } - - humiditySpike(priorRelativeHumidity, forecastRelativeHumidity) { - return (forecastRelativeHumidity / priorRelativeHumidity) > this.humidityChangePct; - } } module.exports = new RulesService(); \ No newline at end of file From ec37bb2004ce39325c53fb9f8ed164926b3c1246 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:05:20 -0400 Subject: [PATCH 07/13] Move to specific humidity --- __tests__/services/metrics.test.js | 2 +- __tests__/services/rules.test.js | 100 +++++++++++++++++++++++++++-- src/services/metrics.js | 2 +- src/services/rules.js | 16 ++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/__tests__/services/metrics.test.js b/__tests__/services/metrics.test.js index 713efe9..1f9820e 100644 --- a/__tests__/services/metrics.test.js +++ b/__tests__/services/metrics.test.js @@ -7,7 +7,7 @@ jest.mock("../../src/repositories/config.js"); gfsRepository.getPrecipitationRate.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); gfsRepository.getPrecipitableWater.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); gfsRepository.getCloudWater.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); -gfsRepository.getSpecificHumidity.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); +gfsRepository.getSpecificHumidity.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); gfsRepository.getGroundTemperature.mockImplementation((lat, lon) => Promise.resolve(mockNumericData)); gfsRepository.getWindSpeed.mockImplementation((lat, lon) => Promise.resolve(mockDecimalData)); diff --git a/__tests__/services/rules.test.js b/__tests__/services/rules.test.js index abca32e..15e6601 100644 --- a/__tests__/services/rules.test.js +++ b/__tests__/services/rules.test.js @@ -21,7 +21,9 @@ describe("Precipitation rate", () => { priorAccumulation: 10.5, forecastAccumulation: 0.0, maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER + maxCloudWater: Number.MAX_SAFE_INTEGER, + priorSpecificHumidity: Number.MAX_SAFE_INTEGER, + forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, }); expect(result).toBe(false); }); @@ -31,7 +33,9 @@ describe("Precipitation rate", () => { priorAccumulation: 0.0, forecastAccumulation: 10.5, maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER + maxCloudWater: Number.MAX_SAFE_INTEGER, + priorSpecificHumidity: Number.MAX_SAFE_INTEGER, + forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, }); expect(result).toBe(false); }); @@ -41,7 +45,9 @@ describe("Precipitation rate", () => { priorAccumulation: 5.5, forecastAccumulation: 5.5, maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER + maxCloudWater: Number.MAX_SAFE_INTEGER, + priorSpecificHumidity: Number.MAX_SAFE_INTEGER, + forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, }); expect(result).toBe(false); }); @@ -51,8 +57,94 @@ describe("Precipitation rate", () => { priorAccumulation: 1.0, forecastAccumulation: 1.0, maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER + maxCloudWater: Number.MAX_SAFE_INTEGER, + priorSpecificHumidity: Number.MAX_SAFE_INTEGER, + forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, }); expect(result).toBe(true); }); }); + +describe("Cloud water", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("Heavy Clouds", async () => { + const result = await rulesService.evaluate({ + priorAccumulation: Number.MAX_SAFE_INTEGER, + forecastAccumulation: Number.MAX_SAFE_INTEGER, + maxPrecipitable: Number.MAX_SAFE_INTEGER, + maxCloudWater: Number.MAX_SAFE_INTEGER, + priorSpecificHumidity: 0.0, + forecastSpecificHumidity: 0.0, + }); + expect(result).toBe(false); + }); + + test("No Clouds", async () => { + const result = await rulesService.evaluate({ + priorAccumulation: Number.MAX_SAFE_INTEGER, + forecastAccumulation: Number.MAX_SAFE_INTEGER, + maxPrecipitable: 0.0, + maxCloudWater: 0.0, + priorSpecificHumidity: 0.0, + forecastSpecificHumidity: 0.0, + }); + expect(result).toBe(true); + }); + + test("Only clouds", async () => { + const result = await rulesService.evaluate({ + priorAccumulation: Number.MAX_SAFE_INTEGER, + forecastAccumulation: Number.MAX_SAFE_INTEGER, + maxPrecipitable: 0.0, + maxCloudWater: Number.MAX_SAFE_INTEGER, + priorSpecificHumidity: 0.0, + forecastSpecificHumidity: 0.0, + }); + expect(result).toBe(false); + }); + + test("Preciptable", async () => { + const result = await rulesService.evaluate({ + priorAccumulation: Number.MAX_SAFE_INTEGER, + forecastAccumulation: Number.MAX_SAFE_INTEGER, + maxPrecipitable: Number.MAX_SAFE_INTEGER, + maxCloudWater: 0.0, + priorSpecificHumidity: 0.0, + forecastSpecificHumidity: 0.0, + }); + expect(result).toBe(false); + }); +}); + +describe("Humidity", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("Humidity spike", async () => { + const result = await rulesService.evaluate({ + priorAccumulation: Number.MAX_SAFE_INTEGER, + forecastAccumulation: Number.MAX_SAFE_INTEGER, + maxPrecipitable:0.0, + maxCloudWater: 0.0, + priorSpecificHumidity: 1.0, + forecastSpecificHumidity: 2.0, + }); + expect(result).toBe(false); + }); + + test("Stable humidity", async () => { + const result = await rulesService.evaluate({ + priorAccumulation: Number.MAX_SAFE_INTEGER, + forecastAccumulation: Number.MAX_SAFE_INTEGER, + maxPrecipitable: 0.0, + maxCloudWater: 0.0, + priorSpecificHumidity: 1.0, + forecastSpecificHumidity: 1.0, + }); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/services/metrics.js b/src/services/metrics.js index 7c33125..0bd7df8 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -32,7 +32,7 @@ class MetricsService { const specificHumidity = data[3] || []; const priorSpecificHumidity = specificHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); const forecastSpecificHumidity = specificHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - if(specificHumidity[0]) console.info(`Least relative humidity (%) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity}`); + if(specificHumidity[0]) console.info(`Least specific humidity (kg/kg) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity}`); const groundTemp = data[4] || []; const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, 0); diff --git a/src/services/rules.js b/src/services/rules.js index 450e213..6e84acf 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -4,17 +4,29 @@ class RulesService { precipitationRateThreshold = configRepository.get("precipitationRateThreshold", 5.0); precipitableWaterThreshold = configRepository.get("precipitableWaterThreshold", 50.0); cloudWaterThreshold = configRepository.get("cloudWaterThreshold", 1.0); - humidityChangePct = configRepository.get("humidityChangePct", 1.3); + humidityChangePct = configRepository.get("humidityChangePct", 1.5); evaluate(facts) { return !( - this.exceedsPrecipitationThreshold(facts.priorAccumulation, facts.forecastAccumulation) + this.exceedsPrecipitationThreshold(facts.priorAccumulation, facts.forecastAccumulation) && + ( + this.exceedsWaterThreshold(facts.maxPrecipitable, facts.maxCloudWater) || + this.humiditySpike(facts.priorSpecificHumidity, facts.forecastSpecificHumidity) + ) ); } + exceedsWaterThreshold(preciptable, cloudWater) { + return (preciptable > this.precipitableWaterThreshold) || (cloudWater > this.cloudWaterThreshold); + } + exceedsPrecipitationThreshold(priorAccumulation, forecastAccumulation) { return (priorAccumulation + forecastAccumulation) > this.precipitationRateThreshold; } + + humiditySpike(priorSpecificHumidity, forecastSpecificHumidity) { + return (forecastSpecificHumidity / priorSpecificHumidity) > this.humidityChangePct; + } } module.exports = new RulesService(); \ No newline at end of file From 018d43e34116269de2bd910cd85a14776dff8762 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:28:30 -0400 Subject: [PATCH 08/13] Calculate evaporation rate --- __tests__/services/metrics.test.js | 7 +++++++ src/services/metrics.js | 23 +++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/__tests__/services/metrics.test.js b/__tests__/services/metrics.test.js index 1f9820e..06b82e0 100644 --- a/__tests__/services/metrics.test.js +++ b/__tests__/services/metrics.test.js @@ -61,6 +61,13 @@ describe("Get forecast metrics", () => { const result = await metricsService.fetch(); expect(result.windSpeed).toBe(0.00003124313318054331); }); + + test("Evaporation rate", async () => { + jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); + const result = await metricsService.fetch(); + //TODO fix the sample data so that this number isn't garbage + expect(result.forecastEvaporationRate).toBe(-1.7472667374812985); + }); }); const mockDecimalData = [ diff --git a/src/services/metrics.js b/src/services/metrics.js index 0bd7df8..0864c57 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -11,8 +11,8 @@ class MetricsService { gfsRepository.getPrecipitationRate(latitude, longitude), gfsRepository.getPrecipitableWater(latitude, longitude), gfsRepository.getCloudWater(latitude, longitude), - gfsRepository.getSpecificHumidity(latitude, longitude), gfsRepository.getGroundTemperature(latitude, longitude), + gfsRepository.getSpecificHumidity(latitude, longitude), gfsRepository.getWindSpeed(latitude, longitude), ]); @@ -29,21 +29,26 @@ class MetricsService { const maxCloudWater = cloudWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); if(cloudWater[0]) console.info(`Maximum cloud water (kg/m^2) @[${cloudWater[0].latitude}, ${cloudWater[0].longitude}]: ${maxCloudWater}`); - const specificHumidity = data[3] || []; - const priorSpecificHumidity = specificHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - const forecastSpecificHumidity = specificHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - if(specificHumidity[0]) console.info(`Least specific humidity (kg/kg) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity}`); - - const groundTemp = data[4] || []; + const groundTemp = data[3] || []; const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, 0); const forecastGroundTemp = groundTemp.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); if(groundTemp[0]) console.info(`Maximum temperature (k) @[${groundTemp[0].latitude}, ${groundTemp[0].longitude}]: ${priorGroundTemp} => ${forecastGroundTemp}`); + const specificHumidity = data[4] || []; + const priorSpecificHumidity = specificHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); + const forecastSpecificHumidity = specificHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); + const forecastGroundTempC = forecastGroundTemp - 273.15; + const maxSpecificHumidity = 0.003733 + (0.00032 * forecastGroundTempC) + (0.000003 * forecastGroundTempC) + (0.0000004 * forecastGroundTempC); + if(specificHumidity[0]) console.info(`Lowest specific humidity (kg/kg) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity} [${maxSpecificHumidity}]`); + const windSpeed = data[5] || []; const futureWindSpeed = windSpeed.filter(result => result.time > now); const avgWindSpeed = futureWindSpeed.reduce((acc, result, i) => ((acc * i) + result.value) / (i+1), 0); if(windSpeed[0]) console.info(`Average wind speed (m/s) @[${windSpeed[0].latitude}, ${windSpeed[0].longitude}]: ${avgWindSpeed}`); + const evaporationRate = (25 + 19 * avgWindSpeed) * 1 * (maxSpecificHumidity - forecastSpecificHumidity); + if(specificHumidity[0]) console.info(`Evaporation rate (kg/h) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${evaporationRate}`); + return { priorAccumulation: priorAccumulation, forecastAccumulation: forecastAccumulation, @@ -51,9 +56,11 @@ class MetricsService { maxCloudWater: maxCloudWater, priorSpecificHumidity: priorSpecificHumidity, forecastSpecificHumidity: forecastSpecificHumidity, + forecastMaxSpecificHumidity: maxSpecificHumidity, priorGroundTemp: priorGroundTemp, forecastGroundTemp: forecastGroundTemp, - windSpeed: avgWindSpeed + windSpeed: avgWindSpeed, + forecastEvaporationRate: evaporationRate, } } } From b9444142f3fffb54330111ab0a7297a97fa7b3d6 Mon Sep 17 00:00:00 2001 From: John Ellis Date: Fri, 6 Sep 2024 19:52:55 -0400 Subject: [PATCH 09/13] Fair warning about my bullcrap math --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 04868a9..4db4549 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ Currently includes a Node.JS app that can be run as a cron entry in order to set rain in the recent past or rain coming up soon. It is assumed that you have already wired a GPIO pin to a relay or a MOSFET so you can switch on or off your irrigation system. +_Fair warning_: if you take a deeper look under the hood you may realize that the math makes no sense, and that I'm +making wild assumptions about weather & soil conditions that often confuse causation and correlation. These are all +valid and good points! The rules engine that determines if the irrigation system should be enabled or disabled was largely +informed by about three months of data collection & subjective observations as summer became fall, so the science +is pretty slim. If you can improve on the logic, submit a pull request or create an issue with a suggested solution +(rather than just pointing out the obvious problems) and I will happily review it! + ## Installing Installing the sprinkler switch requires some hardware installation and installing the SprinklerSwitch scripts: @@ -37,4 +44,4 @@ You can test locally without setting GPIO pins using: ``` npm ci npm start -``` \ No newline at end of file +``` From a5f5f4af630d9dd16bcd03eca345594e008df4e3 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:46:03 -0400 Subject: [PATCH 10/13] Clarify units on evaporation rate --- src/services/metrics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/metrics.js b/src/services/metrics.js index 0864c57..dbb3e00 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -46,8 +46,8 @@ class MetricsService { const avgWindSpeed = futureWindSpeed.reduce((acc, result, i) => ((acc * i) + result.value) / (i+1), 0); if(windSpeed[0]) console.info(`Average wind speed (m/s) @[${windSpeed[0].latitude}, ${windSpeed[0].longitude}]: ${avgWindSpeed}`); - const evaporationRate = (25 + 19 * avgWindSpeed) * 1 * (maxSpecificHumidity - forecastSpecificHumidity); - if(specificHumidity[0]) console.info(`Evaporation rate (kg/h) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${evaporationRate}`); + const evaporationRate = (25 + 19 * avgWindSpeed) * (maxSpecificHumidity - forecastSpecificHumidity); + if(specificHumidity[0]) console.info(`Evaporation rate (kg/m^2/h) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${evaporationRate}`); return { priorAccumulation: priorAccumulation, From 600985a58e39c30094c203b8f60fce1fe8769a21 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:12:06 -0400 Subject: [PATCH 11/13] Cleanup of rules, use evaporation rate --- __tests__/services/metrics.test.js | 2 +- __tests__/services/rules.test.js | 163 +++++++++-------------------- src/services/metrics.js | 38 ++++--- src/services/rules.js | 21 ++-- 4 files changed, 84 insertions(+), 140 deletions(-) diff --git a/__tests__/services/metrics.test.js b/__tests__/services/metrics.test.js index 06b82e0..90097b0 100644 --- a/__tests__/services/metrics.test.js +++ b/__tests__/services/metrics.test.js @@ -66,7 +66,7 @@ describe("Get forecast metrics", () => { jest.useFakeTimers().setSystemTime(new Date('2024-08-20T13:30:00.000Z')); const result = await metricsService.fetch(); //TODO fix the sample data so that this number isn't garbage - expect(result.forecastEvaporationRate).toBe(-1.7472667374812985); + expect(result.forecastEvaporationRate).toBe(-5.247571462687304); }); }); diff --git a/__tests__/services/rules.test.js b/__tests__/services/rules.test.js index 15e6601..6410baf 100644 --- a/__tests__/services/rules.test.js +++ b/__tests__/services/rules.test.js @@ -1,150 +1,89 @@ const configRepository = require("../../src/repositories/config.js"); jest.mock("../../src/repositories/config.js"); -configRepository.get.mockImplementation((key, fallback) => { - switch (key) { - case 'precipitationRateThreshold': return 10.0; - case 'precipitableWaterThreshold': return 50.0; - case 'cloudWaterThreshold': return 1.0; - default: return fallback; - }; -}); - -const rulesService = require("../../src/services/rules.js"); describe("Precipitation rate", () => { - beforeEach(() => { + beforeAll(() => { + configRepository.get.mockImplementation((key, fallback) => { + switch (key) { + default: return fallback; + }; + }); + }); + + afterAll(() => { jest.clearAllMocks(); }); - test("Too much rain yesterday", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: 10.5, - forecastAccumulation: 0.0, - maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER, - priorSpecificHumidity: Number.MAX_SAFE_INTEGER, - forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, + test("Too much rain", async () => { + const rulesService = require("../../src/services/rules.js"); + const result = rulesService.exceedsPrecipitationThreshold({ + priorAccumulation: 1.0, + forecastAccumulation: 0.5, + forecastEvaporationRate: 1.25 }); - expect(result).toBe(false); + expect(result).toBe(true); }); - test("Too much rain today", async () => { - const result = await rulesService.evaluate({ + test("Too little rain", async () => { + const rulesService = require("../../src/services/rules.js"); + const result = rulesService.exceedsPrecipitationThreshold({ priorAccumulation: 0.0, - forecastAccumulation: 10.5, - maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER, - priorSpecificHumidity: Number.MAX_SAFE_INTEGER, - forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, - }); - expect(result).toBe(false); - }); - - test("Too much rain total", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: 5.5, - forecastAccumulation: 5.5, - maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER, - priorSpecificHumidity: Number.MAX_SAFE_INTEGER, - forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, + forecastAccumulation: 0.5, + forecastEvaporationRate: 1.25 }); expect(result).toBe(false); }); +}); - test("Too little rain total", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: 1.0, - forecastAccumulation: 1.0, - maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER, - priorSpecificHumidity: Number.MAX_SAFE_INTEGER, - forecastSpecificHumidity: Number.MAX_SAFE_INTEGER, +describe("Atmosphere conditions", () => { + beforeAll(() => { + configRepository.get.mockImplementation((key, fallback) => { + switch (key) { + case 'precipitableWaterThreshold': return 50.0; + case 'cloudWaterThreshold': return 1.0; + default: return fallback; + }; }); - expect(result).toBe(true); }); -}); -describe("Cloud water", () => { - beforeEach(() => { + afterAll(() => { jest.clearAllMocks(); }); - test("Heavy Clouds", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: Number.MAX_SAFE_INTEGER, - maxCloudWater: Number.MAX_SAFE_INTEGER, - priorSpecificHumidity: 0.0, - forecastSpecificHumidity: 0.0, - }); - expect(result).toBe(false); - }); - - test("No Clouds", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: 0.0, + test("Ready to rain", async () => { + const rulesService = require("../../src/services/rules.js"); + const result = rulesService.exceedsWaterThreshold({ + maxPrecipitable: 51.0, maxCloudWater: 0.0, - priorSpecificHumidity: 0.0, - forecastSpecificHumidity: 0.0, }); expect(result).toBe(true); }); - test("Only clouds", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: 0.0, - maxCloudWater: Number.MAX_SAFE_INTEGER, - priorSpecificHumidity: 0.0, - forecastSpecificHumidity: 0.0, - }); - expect(result).toBe(false); - }); - - test("Preciptable", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable: Number.MAX_SAFE_INTEGER, + test("Dry water column", async () => { + const rulesService = require("../../src/services/rules.js"); + const result = rulesService.exceedsWaterThreshold({ + maxPrecipitable: 10.0, maxCloudWater: 0.0, - priorSpecificHumidity: 0.0, - forecastSpecificHumidity: 0.0, }); expect(result).toBe(false); }); -}); -describe("Humidity", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test("Humidity spike", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, - maxPrecipitable:0.0, - maxCloudWater: 0.0, - priorSpecificHumidity: 1.0, - forecastSpecificHumidity: 2.0, + test("Heavy clouds", async () => { + const rulesService = require("../../src/services/rules.js"); + const result = rulesService.exceedsWaterThreshold({ + maxPrecipitable: 0.0, + maxCloudWater: 1.1, }); - expect(result).toBe(false); + expect(result).toBe(true); }); - test("Stable humidity", async () => { - const result = await rulesService.evaluate({ - priorAccumulation: Number.MAX_SAFE_INTEGER, - forecastAccumulation: Number.MAX_SAFE_INTEGER, + + test("No clouds", async () => { + const rulesService = require("../../src/services/rules.js"); + const result = rulesService.exceedsWaterThreshold({ maxPrecipitable: 0.0, - maxCloudWater: 0.0, - priorSpecificHumidity: 1.0, - forecastSpecificHumidity: 1.0, + maxCloudWater: 0.1, }); - expect(result).toBe(true); + expect(result).toBe(false); }); }); \ No newline at end of file diff --git a/src/services/metrics.js b/src/services/metrics.js index dbb3e00..c95a749 100644 --- a/src/services/metrics.js +++ b/src/services/metrics.js @@ -22,32 +22,31 @@ class MetricsService { if(precipitationRate[0]) console.info(`Total surface precipitation (kg/m^2) @[${precipitationRate[0].latitude}, ${precipitationRate[0].longitude}]: ${priorAccumulation} => ${forecastAccumulation}`); const precipitableWater = data[1] || []; - const maxPrecipitable = precipitableWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); + const maxPrecipitable = precipitableWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, Number.MIN_SAFE_INTEGER); if(precipitableWater[0]) console.info(`Maximum precipitable water (kg/m^2) @[${precipitableWater[0].latitude}, ${precipitableWater[0].longitude}]: ${maxPrecipitable}`); const cloudWater = data[2] || []; - const maxCloudWater = cloudWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); + const maxCloudWater = cloudWater.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, Number.MIN_SAFE_INTEGER); if(cloudWater[0]) console.info(`Maximum cloud water (kg/m^2) @[${cloudWater[0].latitude}, ${cloudWater[0].longitude}]: ${maxCloudWater}`); const groundTemp = data[3] || []; - const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, 0); - const forecastGroundTemp = groundTemp.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, 0); + const priorGroundTemp = groundTemp.reduce((acc, result) => result.time <= now && result.value > acc ? result.value : acc, Number.MIN_SAFE_INTEGER); + const forecastGroundTemp = groundTemp.reduce((acc, result) => result.time > now && result.value > acc ? result.value : acc, Number.MIN_SAFE_INTEGER); if(groundTemp[0]) console.info(`Maximum temperature (k) @[${groundTemp[0].latitude}, ${groundTemp[0].longitude}]: ${priorGroundTemp} => ${forecastGroundTemp}`); const specificHumidity = data[4] || []; const priorSpecificHumidity = specificHumidity.reduce((acc, result) => result.time <= now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); const forecastSpecificHumidity = specificHumidity.reduce((acc, result) => result.time > now && result.value < acc ? result.value : acc, Number.MAX_SAFE_INTEGER); - const forecastGroundTempC = forecastGroundTemp - 273.15; - const maxSpecificHumidity = 0.003733 + (0.00032 * forecastGroundTempC) + (0.000003 * forecastGroundTempC) + (0.0000004 * forecastGroundTempC); - if(specificHumidity[0]) console.info(`Lowest specific humidity (kg/kg) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity} [${maxSpecificHumidity}]`); + if(specificHumidity[0]) console.info(`Lowest specific humidity (kg/kg) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${priorSpecificHumidity} => ${forecastSpecificHumidity}`); const windSpeed = data[5] || []; const futureWindSpeed = windSpeed.filter(result => result.time > now); - const avgWindSpeed = futureWindSpeed.reduce((acc, result, i) => ((acc * i) + result.value) / (i+1), 0); + const avgWindSpeed = futureWindSpeed.reduce((acc, result, i) => ((acc * i) + result.value) / (i+1), Number.MIN_SAFE_INTEGER); if(windSpeed[0]) console.info(`Average wind speed (m/s) @[${windSpeed[0].latitude}, ${windSpeed[0].longitude}]: ${avgWindSpeed}`); - const evaporationRate = (25 + 19 * avgWindSpeed) * (maxSpecificHumidity - forecastSpecificHumidity); - if(specificHumidity[0]) console.info(`Evaporation rate (kg/m^2/h) @[${specificHumidity[0].latitude}, ${specificHumidity[0].longitude}]: ${evaporationRate}`); + const evaporationRate = this.toEvaporationRate(windSpeed, groundTemp, specificHumidity); + const maxEvaporationRate = evaporationRate.reduce((acc, result) => result.time > now && (result.value * result.durationHours) > acc ? (result.value * result.durationHours) : acc, Number.MIN_SAFE_INTEGER); + if(evaporationRate[0]) console.info(`Evaporation rate (kg/m^2) @[${evaporationRate[0].latitude}, ${evaporationRate[0].longitude}]: ${maxEvaporationRate}`); return { priorAccumulation: priorAccumulation, @@ -56,13 +55,28 @@ class MetricsService { maxCloudWater: maxCloudWater, priorSpecificHumidity: priorSpecificHumidity, forecastSpecificHumidity: forecastSpecificHumidity, - forecastMaxSpecificHumidity: maxSpecificHumidity, priorGroundTemp: priorGroundTemp, forecastGroundTemp: forecastGroundTemp, windSpeed: avgWindSpeed, - forecastEvaporationRate: evaporationRate, + forecastEvaporationRate: maxEvaporationRate, } } + + toEvaporationRate(windSpeed, groundTemp, specificHumidity) { + const windByTime = windSpeed.reduce((acc, result) => acc.set(result.time.toISOString(), result), new Map()); + const groundTempByTime = groundTemp.reduce((acc, result) => acc.set(result.time.toISOString(), result), new Map()); + return specificHumidity.map((resultHumid) => { + const resultWind = windByTime.get(resultHumid.time.toISOString()); + const resultTemp = groundTempByTime.get(resultHumid.time.toISOString()); + const resultTempC = resultTemp.value - 273.15; + const maxSpecificHumidity = 0.003733 + (0.00032 * resultTempC) + (0.000003 * resultTempC) + (0.0000004 * resultTempC); + return { + ...resultHumid, + durationHours: resultHumid.duration / (60 * 60), + value: (25 + 19 * resultWind.value) * (maxSpecificHumidity - resultHumid.value) + } + }); + } } module.exports = new MetricsService(); \ No newline at end of file diff --git a/src/services/rules.js b/src/services/rules.js index 6e84acf..83501bc 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -1,31 +1,22 @@ const configRepository = require("../repositories/config.js"); class RulesService { - precipitationRateThreshold = configRepository.get("precipitationRateThreshold", 5.0); precipitableWaterThreshold = configRepository.get("precipitableWaterThreshold", 50.0); cloudWaterThreshold = configRepository.get("cloudWaterThreshold", 1.0); - humidityChangePct = configRepository.get("humidityChangePct", 1.5); evaluate(facts) { return !( - this.exceedsPrecipitationThreshold(facts.priorAccumulation, facts.forecastAccumulation) && - ( - this.exceedsWaterThreshold(facts.maxPrecipitable, facts.maxCloudWater) || - this.humiditySpike(facts.priorSpecificHumidity, facts.forecastSpecificHumidity) - ) + this.exceedsPrecipitationThreshold(facts) && + this.exceedsWaterThreshold(facts) ); } - exceedsWaterThreshold(preciptable, cloudWater) { - return (preciptable > this.precipitableWaterThreshold) || (cloudWater > this.cloudWaterThreshold); + exceedsWaterThreshold(facts) { + return (facts.maxPrecipitable > this.precipitableWaterThreshold) || (facts.maxCloudWater > this.cloudWaterThreshold); } - exceedsPrecipitationThreshold(priorAccumulation, forecastAccumulation) { - return (priorAccumulation + forecastAccumulation) > this.precipitationRateThreshold; - } - - humiditySpike(priorSpecificHumidity, forecastSpecificHumidity) { - return (forecastSpecificHumidity / priorSpecificHumidity) > this.humidityChangePct; + exceedsPrecipitationThreshold(facts) { + return (facts.priorAccumulation + facts.forecastAccumulation) > facts.forecastEvaporationRate; } } From 38a2e6ccf4ad12859e603d87b8938772e2750e1b Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:14:45 -0400 Subject: [PATCH 12/13] Clarify rules --- __tests__/services/rules.test.js | 16 ++++++++-------- src/services/rules.js | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/__tests__/services/rules.test.js b/__tests__/services/rules.test.js index 6410baf..0dc6c78 100644 --- a/__tests__/services/rules.test.js +++ b/__tests__/services/rules.test.js @@ -1,7 +1,7 @@ const configRepository = require("../../src/repositories/config.js"); jest.mock("../../src/repositories/config.js"); -describe("Precipitation rate", () => { +describe("Determine if we have had enough rain", () => { beforeAll(() => { configRepository.get.mockImplementation((key, fallback) => { switch (key) { @@ -16,7 +16,7 @@ describe("Precipitation rate", () => { test("Too much rain", async () => { const rulesService = require("../../src/services/rules.js"); - const result = rulesService.exceedsPrecipitationThreshold({ + const result = rulesService.isRainSufficient({ priorAccumulation: 1.0, forecastAccumulation: 0.5, forecastEvaporationRate: 1.25 @@ -26,7 +26,7 @@ describe("Precipitation rate", () => { test("Too little rain", async () => { const rulesService = require("../../src/services/rules.js"); - const result = rulesService.exceedsPrecipitationThreshold({ + const result = rulesService.isRainSufficient({ priorAccumulation: 0.0, forecastAccumulation: 0.5, forecastEvaporationRate: 1.25 @@ -35,7 +35,7 @@ describe("Precipitation rate", () => { }); }); -describe("Atmosphere conditions", () => { +describe("Determine if rain is expected", () => { beforeAll(() => { configRepository.get.mockImplementation((key, fallback) => { switch (key) { @@ -52,7 +52,7 @@ describe("Atmosphere conditions", () => { test("Ready to rain", async () => { const rulesService = require("../../src/services/rules.js"); - const result = rulesService.exceedsWaterThreshold({ + const result = rulesService.isRainExpected({ maxPrecipitable: 51.0, maxCloudWater: 0.0, }); @@ -61,7 +61,7 @@ describe("Atmosphere conditions", () => { test("Dry water column", async () => { const rulesService = require("../../src/services/rules.js"); - const result = rulesService.exceedsWaterThreshold({ + const result = rulesService.isRainExpected({ maxPrecipitable: 10.0, maxCloudWater: 0.0, }); @@ -70,7 +70,7 @@ describe("Atmosphere conditions", () => { test("Heavy clouds", async () => { const rulesService = require("../../src/services/rules.js"); - const result = rulesService.exceedsWaterThreshold({ + const result = rulesService.isRainExpected({ maxPrecipitable: 0.0, maxCloudWater: 1.1, }); @@ -80,7 +80,7 @@ describe("Atmosphere conditions", () => { test("No clouds", async () => { const rulesService = require("../../src/services/rules.js"); - const result = rulesService.exceedsWaterThreshold({ + const result = rulesService.isRainExpected({ maxPrecipitable: 0.0, maxCloudWater: 0.1, }); diff --git a/src/services/rules.js b/src/services/rules.js index 83501bc..26b8ac7 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -6,16 +6,16 @@ class RulesService { evaluate(facts) { return !( - this.exceedsPrecipitationThreshold(facts) && - this.exceedsWaterThreshold(facts) + this.isRainSufficient(facts) || + this.isRainExpected(facts) ); } - exceedsWaterThreshold(facts) { + isRainExpected(facts) { return (facts.maxPrecipitable > this.precipitableWaterThreshold) || (facts.maxCloudWater > this.cloudWaterThreshold); } - exceedsPrecipitationThreshold(facts) { + isRainSufficient(facts) { return (facts.priorAccumulation + facts.forecastAccumulation) > facts.forecastEvaporationRate; } } From ba892263ed6f6ac6f4f28a0507136278da47e757 Mon Sep 17 00:00:00 2001 From: John Ellis <532789+deckerego@users.noreply.github.com> Date: Sun, 8 Sep 2024 10:33:49 -0400 Subject: [PATCH 13/13] Added the "Terf Nerf" factor --- src/repositories/gfs.js | 5 ++--- src/services/rules.js | 11 ++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/repositories/gfs.js b/src/repositories/gfs.js index 9b51d9c..27311b9 100644 --- a/src/repositories/gfs.js +++ b/src/repositories/gfs.js @@ -54,11 +54,10 @@ class GfsRepository { async getAggregateMetric(lat, lon, metric) { const metrics = await this.getMetric(lat, lon, metric); - if(DEBUG) console.debug(`Fetched ${metrics.array_format.length} ${metric} results from NOAA`); if(TRACE) console.trace(metrics.array_format.map(result => `${new Date(result.time).toISOString()},${result.lat},${result.lon},${metric},${result.value}`)); const aggregateMetrics = GfsRepository.closest(lat, lon, metrics); - if(DEBUG) console.debug(`Aggregated ${aggregateMetrics.length} ${metric} values`); + if(DEBUG) console.debug(`Aggregated ${metric} metrics from ${metrics.array_format.length} to ${aggregateMetrics.length} values`); if(TRACE) console.trace(aggregateMetrics.map(result => `${new Date(result.time).toISOString()},${metric},${result.duration},${result.value}`)); return aggregateMetrics; @@ -66,7 +65,7 @@ class GfsRepository { async getMetric(lat, lon, metric) { const yesterday = GfsRepository.getYesterday(); - if(! TRACE) console.log = (message) => { /* Mute console logging from the NOAA GFS library */ }; + if(! DEBUG) console.log = (message) => { /* Mute console logging from the NOAA GFS library */ }; const result = await noaa_gfs.get_gfs_data(this.precision, yesterday.dateString, yesterday.hourString, [lat, lat], [lon, lon], this.sampleCount, metric, true); console.log = consoleLog; return result; diff --git a/src/services/rules.js b/src/services/rules.js index 26b8ac7..01fb37e 100644 --- a/src/services/rules.js +++ b/src/services/rules.js @@ -3,20 +3,21 @@ const configRepository = require("../repositories/config.js"); class RulesService { precipitableWaterThreshold = configRepository.get("precipitableWaterThreshold", 50.0); cloudWaterThreshold = configRepository.get("cloudWaterThreshold", 1.0); + turfNerf = configRepository.get("soilRetentionFactor", 0.5); // How much do we think grass reduces the evaporation rate? evaluate(facts) { return !( - this.isRainSufficient(facts) || - this.isRainExpected(facts) + this.isRainSufficient(facts) || // Is rain expected to happen in the near future, OR + this.isRainExpected(facts) // Did we get enough rain in the recent past? ); } - isRainExpected(facts) { + isRainExpected(facts) { return (facts.maxPrecipitable > this.precipitableWaterThreshold) || (facts.maxCloudWater > this.cloudWaterThreshold); } - isRainSufficient(facts) { - return (facts.priorAccumulation + facts.forecastAccumulation) > facts.forecastEvaporationRate; + isRainSufficient(facts) { + return (facts.priorAccumulation + facts.forecastAccumulation) >= (facts.forecastEvaporationRate * this.turfNerf); } }