From 9b6458ce39982ff81e9be7cb1e4cbb8330828e83 Mon Sep 17 00:00:00 2001 From: Brandon Satrom Date: Thu, 27 Apr 2023 21:06:54 -0500 Subject: [PATCH 1/2] reworked NF8 example --- .../firmware/notepower/NoteRTOS.h | 129 --- .../firmware/notepower/TicksTimer.h | 36 - .../firmware/notepower/app-name.h | 2 - .../firmware/notepower/app.cpp | 830 ------------------ .../firmware/notepower/app.h | 58 +- .../firmware/notepower/metadata.h | 48 + .../firmware/notepower/notepower.ino | 408 ++++++++- .../firmware/notepower/power_monitor.h | 35 + .../firmware/platformio.ini | 8 +- 9 files changed, 473 insertions(+), 1081 deletions(-) delete mode 100644 08-power-quality-monitor/firmware/notepower/NoteRTOS.h delete mode 100644 08-power-quality-monitor/firmware/notepower/TicksTimer.h delete mode 100644 08-power-quality-monitor/firmware/notepower/app-name.h delete mode 100644 08-power-quality-monitor/firmware/notepower/app.cpp create mode 100644 08-power-quality-monitor/firmware/notepower/metadata.h create mode 100644 08-power-quality-monitor/firmware/notepower/power_monitor.h diff --git a/08-power-quality-monitor/firmware/notepower/NoteRTOS.h b/08-power-quality-monitor/firmware/notepower/NoteRTOS.h deleted file mode 100644 index 792933f1..00000000 --- a/08-power-quality-monitor/firmware/notepower/NoteRTOS.h +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2022 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -#include -#include -#include - -#pragma once - -// Init function -bool _init(void); - -// Mutex functions -void _lock_wire(void); -void _unlock_wire(void); -void _lock_note(void); -void _unlock_note(void); - -// Basic allocation functions -void *_malloc(size_t size); -void _free(void *); - -// Time functions -void _delay(uint32_t ms); -uint32_t _millis(void); - -// Include the implementation -#ifdef APP_MAIN - -#if defined(INC_FREERTOS_H) - -SemaphoreHandle_t _wireMutex; -SemaphoreHandle_t _noteMutex; -bool _init() -{ - _wireMutex = xSemaphoreCreateMutex(); - if (_wireMutex == NULL) { - return false; - } - _noteMutex = xSemaphoreCreateMutex(); - if (_noteMutex == NULL) { - return false; - } - return true; -} -void _lock_wire() -{ - xSemaphoreTake(_wireMutex, portMAX_DELAY); -} -void _unlock_wire() -{ - xSemaphoreGive(_wireMutex); -} -void _lock_note() -{ - xSemaphoreTake(_noteMutex, portMAX_DELAY); -} -void _unlock_note() -{ - xSemaphoreGive(_noteMutex); -} -void *_malloc(size_t size) -{ - return pvPortMalloc(size); -} -void _free(void *p) -{ - vPortFree(p); -} -void _delay(uint32_t ms) -{ - vTaskDelay((uint32_t)((((uint64_t) ms * configTICK_RATE_HZ)) / 1000LL)); -} -uint32_t _millis(void) -{ - return (uint32_t) ((((uint64_t) xTaskGetTickCount()) * 1000LL) / configTICK_RATE_HZ); -} - -#elif defined(ARDUINO) - -void _lock_wire() {} -void _unlock_wire() {} -void _lock_note() {} -void _unlock_note() {} -bool _init() -{ - return true; -} -void *_malloc(size_t size) -{ - return malloc(size); -} -void _free(void *p) -{ - free(p); -} -void _delay(uint32_t ms) -{ - delay(ms); -} -uint32_t _millis(void) -{ - return millis(); -} - -#else - -__attribute__((weak)) void _lock_wire() {} -__attribute__((weak)) void _unlock_wire() {} -__attribute__((weak)) void _lock_note() {} -__attribute__((weak)) void _unlock_note() {} -void *_malloc(size_t size) -{ - return malloc(size); -} -void _free(void *p) -{ - free(p); -} -__attribute__((weak)) void _delay(uint32_t ms) {} -__attribute__((weak)) uint32_t _millis(void) -{ - return 0; -} - -#endif // Which RTOS - -#endif // APP_MAIN diff --git a/08-power-quality-monitor/firmware/notepower/TicksTimer.h b/08-power-quality-monitor/firmware/notepower/TicksTimer.h deleted file mode 100644 index 21adb160..00000000 --- a/08-power-quality-monitor/firmware/notepower/TicksTimer.h +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include "Arduino.h" - -/** - * @brief Provides a duration timer that can measure durations up to 2^31-1 milliseconds safely with - * wrap around when the ticks counter resets to 0 after 2^32 ticks. - */ -struct TicksTimer { - - typedef uint32_t ticks_t; - - ticks_t start_time = 0; - ticks_t duration = 0; - - inline void set(ticks_t start_time, ticks_t duration) { - this->start_time = start_time; - this->duration = duration; - } - - inline bool hasElapsed(ticks_t current_ticks) { - return (current_ticks-start_time)>duration; - } -}; - -/** - * @brief Convenience class that implements TicksTimer using arduino millis(). - */ -struct ArduinoTicksTimer: public TicksTimer { - inline void set(ticks_t duration) { - TicksTimer::set(millis(), duration); - } - - inline bool hasElapsed() { - return TicksTimer::hasElapsed(millis()); - } -}; diff --git a/08-power-quality-monitor/firmware/notepower/app-name.h b/08-power-quality-monitor/firmware/notepower/app-name.h deleted file mode 100644 index e7db149d..00000000 --- a/08-power-quality-monitor/firmware/notepower/app-name.h +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -#define APP_NAME "nf8" \ No newline at end of file diff --git a/08-power-quality-monitor/firmware/notepower/app.cpp b/08-power-quality-monitor/firmware/notepower/app.cpp deleted file mode 100644 index 92048564..00000000 --- a/08-power-quality-monitor/firmware/notepower/app.cpp +++ /dev/null @@ -1,830 +0,0 @@ -// Copyright 2022 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -#include "app.h" -#include "UpbeatLabs_MCP39F521.h" -#include "TicksTimer.h" - -// MCP (Dr Wattson) Hardware definitions -#define MCP_I2C_ADDRESS_BASE 0x74 -#define MCP_I2C_INSTANCES 4 -#define REPORT_REASONS_LENGTH (256) - -// Switched outputs or sensed inputs -typedef struct { - const char *ovar; // output variable name - const char *ivar; // input variable name - int pin; - bool on; - bool output; // pin mode is output when true, input when false. - bool init; -} pindef; - -pindef ioPin[] = { - {.ovar = "switch1", .ivar = "input1", .pin = D10, .on = false, .init = false}, - {.ovar = "switch2", .ivar = "input2", .pin = D11, .on = false, .init = false}, - {.ovar = "switch3", .ivar = "input3", .pin = D12, .on = false, .init = false}, - {.ovar = "switch4", .ivar = "input4", .pin = D13, .on = false, .init = false}, -}; -#define ioPins ((int)(sizeof(ioPin)/sizeof(ioPin[0]))) - - -// Notefile/Note definitions -#define DATA_FILENAME "power.qo" -#define DATA_FIELD_APP "app" -#define DATA_FIELD_ALERT "alert" -#define DATA_FIELD_INSTANCE "instance" -#define DATA_FIELD_VOLTAGE "voltage" -#define DATA_FIELD_CURRENT "current" -#define DATA_FIELD_POWER "power" -#define DATA_FIELD_FREQUENCY "frequency" -#define DATA_FIELD_REACTIVE "reactivePower" -#define DATA_FIELD_APPARENT "apparentPower" -#define DATA_FIELD_POWERFACTOR "powerFactor" -#define DATA_FIELD_PIN_ACTIVE "pinActive" -#define DATA_FIELD_VIBRATION "vibration" -#define DATA_FIELD_VIBRATION_RAW "vibration_raw" -#define DATA_FIELD_EVENT_COUNTER "counter" - -// Cached copies of environment variables -uint32_t envHeartbeatMins = 0; -float envVoltageUnder = 0; -float envVoltageOver = 0; -float envVoltageChange = 0; -float envCurrentUnder = 0; -float envCurrentOver = 0; -float envCurrentChange = 0; -float envPowerUnder = 0; -float envPowerOver = 0; -float envPowerChange = 0; -float envVibrationOff = 0; -float envVibrationUnder = 0; -float envVibrationOver = 0; -int8_t envVibrationActiveLine = 0; - -// Time used to determine whether or not we should refresh the environment vars -int64_t environmentModifiedTime = 0; -float vibrationAccumulator = 0; -int32_t vibrationCount = 0; -const int32_t vibrationSamples = 5; -float vibration = 0; -int32_t eventCounter = 0; - -/** - * @brief Describes the type of power for an MCP, which is correlated with the activity state of - * the corresponding GPIO pin. - */ -enum class PowerActivityAlert { - NONE, /* no alerts related to activity of the line pin */ - LOAD, /* power is supplied to a load, should be 0 power consumed when inactive */ - SUPPLY /* power is provided by a supply, current and voltage should be 0 when off. */ -}; - -// MCP-specific context used to manage instances. -typedef struct { - uint8_t taskID; - uint8_t i2cAddress; - UpbeatLabs_MCP39F521 wattson; - float lastApparentPower; - float lastReactivePower; - float lastPowerFactor; - float lastVoltage; - float lastCurrent; - float lastPower; - float maxVoltage; - float maxCurrent; - float maxPower; - JTIME heartbeatDue; - uint32_t heartbeatMins; - PowerActivityAlert activityAlert; - float startup; // duration in seconds - float shutdown; // duration in seconds - ArduinoTicksTimer suppressActivityAlarmUntil; // when alarm suppression begins - bool first; - char lastReasons[REPORT_REASONS_LENGTH]; -} mcpContext; -mcpContext mcp[MCP_I2C_INSTANCES]; -uint32_t mcpSchedMs[MCP_I2C_INSTANCES]; - -static_assert(MCP_I2C_INSTANCES==ioPins, "Each MCP instance should have a dedicated IO pin."); - -// Forwards -bool refreshEnvironmentVars(void); -void updateEnvironment(J *body); -float computeVibrationFromAccelerometer(int x, int y, int z); - -// Dynamically sense the instance(s) of the device that are present -uint32_t appTasks(uint32_t **taskSchedMs, uint8_t **contextBase, uint32_t *contextSize) -{ - - // Detect MCP instances - uint8_t tasks = 0; - for (;;) { - _lock_wire(); - for (uint8_t i=0; i<=MCP_I2C_INSTANCES; i++) { - Wire.beginTransmission(MCP_I2C_ADDRESS_BASE + i); - if (Wire.endTransmission() == 0) { - mcp[tasks].taskID = i; - mcp[tasks].i2cAddress = MCP_I2C_ADDRESS_BASE + i; - mcp[tasks].lastReasons[0] = 0; - debug.printf("mcp %d found at i2c 0x%02x\n", mcp[tasks].taskID, mcp[tasks].i2cAddress); - _delay(100); - tasks++; - } - } - _unlock_wire(); - if (tasks > 0) { - break; - } - debug.printf("waiting for connection to an MCP instance\n"); - _delay(1000); - Wire.end(); - Wire.begin(); - } - - // Done - *taskSchedMs = mcpSchedMs; - *contextBase = (uint8_t *) mcp; - *contextSize = sizeof(mcpContext); - return tasks; - -} - -// Set up the Notecard in preparation for the mcp task -bool appSetup(void) -{ - - // Initialize the Notecard for I2C and for the rtos - J *req = notecard.newRequest("hub.set"); - JAddStringToObject(req, "product", PRODUCT_UID); - JAddStringToObject(req, "mode", "periodic"); - JAddNumberToObject(req, "inbound", 60*24); - JAddNumberToObject(req, "outbound", 60); - if (!notecard.sendRequest(req)) { - debug.printf("notecard not responding\n"); - return false; - } - - // Add the notefile template - J *body = JCreateObject(); - JAddStringToObject(body, DATA_FIELD_APP, TSTRINGV); - JAddStringToObject(body, DATA_FIELD_ALERT, TSTRINGV); - JAddNumberToObject(body, DATA_FIELD_INSTANCE, TUINT8); - JAddNumberToObject(body, DATA_FIELD_VOLTAGE, TFLOAT32); - JAddNumberToObject(body, DATA_FIELD_CURRENT, TFLOAT32); - JAddNumberToObject(body, DATA_FIELD_POWER, TFLOAT32); - JAddNumberToObject(body, DATA_FIELD_FREQUENCY, TFLOAT16); - JAddNumberToObject(body, DATA_FIELD_APPARENT, TFLOAT32); - JAddNumberToObject(body, DATA_FIELD_REACTIVE, TFLOAT32); - JAddNumberToObject(body, DATA_FIELD_POWERFACTOR, TFLOAT16); - JAddNumberToObject(body, DATA_FIELD_VIBRATION_RAW, TFLOAT16); - JAddStringToObject(body, DATA_FIELD_VIBRATION, TSTRINGV); - JAddBoolToObject(body, DATA_FIELD_PIN_ACTIVE, TBOOL); - JAddNumberToObject(body, DATA_FIELD_EVENT_COUNTER, TUINT32); - - req = notecard.newCommand("note.template"); - JAddStringToObject(req, "file", DATA_FILENAME); - JAddItemToObject(req, "body", body); - notecard.sendRequest(req); - - // Set the AUX port into "receive notifications" mode - if (serialIsAvailable()) { -#if serialIsAvailable() && SERIAL_RX_BUFFER_SIZE < 4096 -#error INSUFFICIENT SERIAL BUFFER RX SIZE (SEE ARDUINO.txt) -#endif - Serial.begin(115200); - req = notecard.newCommand("card.aux.serial"); - JAddStringToObject(req, "mode", "notify,env,accel"); - JAddNumberToObject(req, "duration", 500); - notecard.sendRequest(req); - } - - // Load the environment vars for the first time - refreshEnvironmentVars(); - - // Done - return true; - -} - -// Perform repetitive tasks -uint32_t appLoop(void) -{ - - // If we're not updating via serial notify, poll to see if the environment vars have been modified - if (!serialIsAvailable()) { - - J *rsp = notecard.requestAndResponse(notecard.newRequest("env.modified")); - if (rsp != NULL) { - if (!notecard.responseError(rsp) && environmentModifiedTime != (int64_t) JGetNumber(rsp, "time")) { - refreshEnvironmentVars(); - } - notecard.deleteResponse(rsp); - } - - // Poll only occasionally - return 15000; - - } - - // See if we've got any data available on the serial port - if (!Serial.available()) { - return 0; - } - - // Define the scope of the string received from Serial so it's freed immediately after parsing - J *notification; - { - String receivedString; - - // Receive a JSON object over the serial line - receivedString = Serial.readStringUntil('\n'); - if (receivedString == NULL) { - return 0; - } - - // Parse the JSON object - const char *JSON = receivedString.c_str(); - debug.printf("notification: %s\n", JSON); - notification = JParse(JSON); - if (notification == NULL) { - debug.printf("notify: not a JSON object\n"); - return 0; - } - } - - // Get notification type, and ignore if not an "env" notification - const char *notificationType = JGetString(notification, "type"); - if (!strcmp(notificationType, "env")) { - // Update the env modified time - environmentModifiedTime = JGetNumber(notification, "modified"); - - // Update the environment - J *body = JGetObject(notification, "body"); - if (body != NULL) { - updateEnvironment(body); - } - } - else if (!strcmp(notificationType, "accel")) { - int x = JGetNumber(notification, "x"); - int y = JGetNumber(notification, "y"); - int z = JGetNumber(notification, "z"); - vibrationAccumulator += computeVibrationFromAccelerometer(x, y, z); - vibrationCount++; - if (vibrationCount>=vibrationSamples) { - vibration = vibrationAccumulator/vibrationCount; - vibrationAccumulator = 0; - vibrationCount = 0; - } - } - else { - debug.printf("notify: ignoring '%s'\n", notificationType); - } - // Done - JDelete(notification); - return 0; -} - -// Per-task setup - each task handles a distinct Dr Wattson instance. -bool taskSetup(void *vmcp) -{ - mcpContext *mcp = (mcpContext *) vmcp; - - // Initialize state - mcp->first = true; - mcp->heartbeatDue = 0; - mcp->heartbeatMins = 0; - - // Initialize communications with the card - _lock_wire(); - mcp->wattson = UpbeatLabs_MCP39F521(); - mcp->wattson.begin(mcp->i2cAddress); - _unlock_wire(); - - // Done - return true; - -} - -float computeVibrationFromAccelerometer(int x, int y, int z) -{ - // remove acceleration due to gravity - z -= 1024; - return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); -} - -void updatePinState(pindef& ioPin) { - if (ioPin.init && !ioPin.output) { - ioPin.on = digitalRead(ioPin.pin)==HIGH; - } -} - -// Nominal voltage/current for 0. Detection is not totally precise and there is some inherent noise/drift so these are -// set to be just above 0. -#define ZERO_VOLTS (5) -#define ZERO_AMPS (0.3) - -/** - * @brief Describes the types of check that is performed in relation to - * - */ -enum class PowerCheck { - NORMAL, /* no additional checks for power being present are done beyond the under/over checks */ - PRESENT, /* Power is expected since the activation pin indicates the device is active */ - NOT_PRESENT, /* Power is not expected since the activation pin indicates the device is not active */ -}; - -/** - * @brief Determines the type of voltage check that is applicable to a monitored machine based on whether - * the activity line is configured, and the nature of the power source. - * - * @param mcp - * @param pin - * @return PowerCheck - */ -PowerCheck voltageActivityCheckRequired(mcpContext* mcp, pindef& pin) { - if (!pin.init) { - // activity monitoring requires the corresponding pin to be configured - return PowerCheck::NORMAL; - } - PowerCheck result; - switch (mcp->activityAlert) { - case PowerActivityAlert::NONE: result = PowerCheck::NORMAL; break; // activity checks not configured for this line - case PowerActivityAlert::LOAD: result = PowerCheck::PRESENT; break; // load is always supplied with voltage - case PowerActivityAlert::SUPPLY: result = pin.on ? PowerCheck::PRESENT : PowerCheck::NOT_PRESENT; break; // supply provides voltage when on - } - return result; -} - -/** - * @brief Determines the type of check for current required for a given line. - * - * @param mcp - * @param pin - * @return PowerCheck - */ -PowerCheck currentActivityCheckRequired(mcpContext* mcp, pindef& pin) { - if (!pin.init || mcp->activityAlert==PowerActivityAlert::NONE) { - // activity monitoring requires the corresponding pin to be configured - return PowerCheck::NORMAL; - } - PowerCheck result; - switch (mcp->activityAlert) { - case PowerActivityAlert::NONE: result = PowerCheck::NORMAL; break; - // load is expected to draw current while it's on - case PowerActivityAlert::LOAD: result = pin.on ? PowerCheck::PRESENT : PowerCheck::NOT_PRESENT; break; - // supply may or may not be showing a current measurement depending on the activity of the load - case PowerActivityAlert::SUPPLY: result = PowerCheck::NORMAL; break; - } - return result; -} - -/** - * @brief Describes the level of vibration detected. - */ -enum VibrationCategory { - VIBRATION_NOT_MEASURED = 0, - VIBRATION_NONE, - VIBRATION_LOW, // can't call this LOW because of Arduino #defines. - VIBRATION_NORMAL, - VIBRATION_HIGH -}; - -/** - * @brief Determines if vibration monitoring is required. - * @return true Vibration monitoring is needed. - * @return false Vibration monitoring is not needed. - */ -inline bool isMonitoringVibration() { - return vibration!=0.0 && (envVibrationOff != 0.0 || envVibrationUnder != 0.0 || envVibrationOver != 0.0 - && (envVibrationOff <= envVibrationUnder) && (envVibrationUnder < envVibrationOver)); -} - -/** - * @brief Determines the vibration category corresponding to the vibration sensed. - * @return VibrationCategory - */ -VibrationCategory determineVibrationCategory() { - if (!isMonitoringVibration()) - return VIBRATION_NOT_MEASURED; - if (vibration <= envVibrationOff) { - return VIBRATION_NONE; - } - if (vibration <= envVibrationUnder) { - return VIBRATION_LOW; - } - if (vibration < envVibrationOver) { - return VIBRATION_NORMAL; - } - return VIBRATION_HIGH; -} - -const char* vibrationCategoryString(VibrationCategory category) { - switch (category) { - case VIBRATION_NOT_MEASURED: return "N/A"; - case VIBRATION_NONE: return "none"; - case VIBRATION_LOW: return "low"; - case VIBRATION_NORMAL: return "normal"; - case VIBRATION_HIGH: return "high"; - } - return ""; -} - -// Per-task loop -uint32_t taskLoop(void *vmcp) -{ - mcpContext *mcp = (mcpContext *) vmcp; - - // This is how quickly we should come back in normal circumstances. We choose - // this value because it tends to be good enough to debounce the 'rise' or 'fall' - // when something is powering on and off. - uint32_t quickly = 250; - - // If it's time to send a heartbeat, do so - bool reportHeartbeat = false; - if (mcp->heartbeatMins != envHeartbeatMins) { - mcp->heartbeatDue = 0; - mcp->heartbeatMins = envHeartbeatMins; - } - if (mcp->heartbeatMins != 0 && NoteTimeValidST()) { - JTIME now = NoteTimeST(); - if (now > mcp->heartbeatDue) { - mcp->heartbeatDue = now + (mcp->heartbeatMins * 60); - reportHeartbeat = true; - } - } - - // Read data - _lock_wire(); - UpbeatLabs_MCP39F521_Data rawData; - int ret = mcp->wattson.read(&rawData, NULL); - _unlock_wire(); - if (ret != UpbeatLabs_MCP39F521::SUCCESS) { - Wire.end(); - Wire.begin(); - debug.printf("*** error reading sensor data: %d\n", ret); - return 2500; - } - - // Convert data to something usable - UpbeatLabs_MCP39F521_FormattedData data; - mcp->wattson.convertRawData(&rawData, &data); - if (mcp->first) { - mcp->first = false; - mcp->maxVoltage = mcp->lastVoltage = data.voltageRMS; - mcp->maxCurrent = mcp->lastCurrent = data.currentRMS; - mcp->maxPower = mcp->lastPower = data.activePower; - } - - for (int i=0; itaskID]; - PowerCheck currentActivityCheck = currentActivityCheckRequired(mcp, pin); - PowerCheck voltageActivityCheck = voltageActivityCheckRequired(mcp, pin); - - // Determine if anything requires an alert. When non-zero length, the first character is a comma, and can be ignored - char reportReasons[REPORT_REASONS_LENGTH] = {'\0'}; - if (voltageActivityCheck != PowerCheck::NOT_PRESENT) { - if (envVoltageUnder != 0 && data.voltageRMS < envVoltageUnder) { - strlcat(reportReasons, ",undervoltage", sizeof(reportReasons)); - } - if (envVoltageOver != 0 && data.voltageRMS > envVoltageOver) { - strlcat(reportReasons, ",overvoltage", sizeof(reportReasons)); - } - float d = mcp->maxVoltage * (envVoltageChange * 0.01) < 1 ? 1 : mcp->maxVoltage * (envVoltageChange * 0.01); - if (envVoltageChange != 0 && ((data.voltageRMS < mcp->lastVoltage-d) || data.voltageRMS > mcp->lastVoltage+d)) { - strlcat(reportReasons, ",voltage", sizeof(reportReasons)); - } - if (voltageActivityCheck==PowerCheck::PRESENT && data.voltageRMS <= ZERO_VOLTS) { - strlcat(reportReasons, ",novoltage", sizeof(reportReasons)); - } - } - else { - if (data.voltageRMS > ZERO_VOLTS) { - strlcat(reportReasons, ",inactivevoltage", sizeof(reportReasons)); - } - } - - if (currentActivityCheck != PowerCheck::NOT_PRESENT) { - if (envCurrentUnder != 0 && data.currentRMS < envCurrentUnder) { - strlcat(reportReasons, ",undercurrent", sizeof(reportReasons)); - } - if (envCurrentOver != 0 && data.currentRMS > envCurrentOver) { - strlcat(reportReasons, ",overcurrent", sizeof(reportReasons)); - } - float d = mcp->maxCurrent * (envCurrentChange * 0.01) < 1 ? 1 : mcp->maxCurrent * (envCurrentChange * 0.01); - if (envCurrentChange != 0 && ((data.currentRMS < mcp->lastCurrent-d) || data.currentRMS > mcp->lastCurrent+d)) { - strlcat(reportReasons, ",current", sizeof(reportReasons)); - } - if (envPowerUnder != 0 && data.activePower < envPowerUnder) { - strlcat(reportReasons, ",underpower", sizeof(reportReasons)); - } - if (envPowerOver != 0 && data.activePower > envPowerOver) { - strlcat(reportReasons, ",overpower", sizeof(reportReasons)); - } - d = mcp->maxPower * (envPowerChange * 0.01) < 1 ? 1 : mcp->maxPower * (envPowerChange * 0.01); - if (envPowerChange != 0 && ((data.activePower < mcp->lastPower-d) || data.activePower > mcp->lastPower+d)) { - strlcat(reportReasons, ",power", sizeof(reportReasons)); - } - if (currentActivityCheck == PowerCheck::PRESENT && data.currentRMS <= ZERO_AMPS) { - strlcat(reportReasons, ",nocurrent", sizeof(reportReasons)); - } - } - else { - if (data.currentRMS > ZERO_AMPS) { - strlcat(reportReasons, ",inactivecurrent", sizeof(reportReasons)); - } - } - - bool vibrationAlert = vibrationCategory && - (((envVibrationActiveLine==0 || (envVibrationActiveLine==mcp->taskID+1 && !pin.init)) && (vibrationCategory==VIBRATION_LOW || vibrationCategory==VIBRATION_HIGH)) || - (envVibrationActiveLine==mcp->taskID+1 && pin.init && (vibrationCategory!=(pin.on ? VIBRATION_NORMAL : VIBRATION_NONE)))); - if (vibrationAlert) { - strlcat(reportReasons, ",vibration", sizeof(reportReasons)); - } - - // Remember state for next time - mcp->lastVoltage = data.voltageRMS; - if (mcp->lastVoltage > mcp->maxVoltage) { - mcp->maxVoltage = mcp->lastVoltage; - } - mcp->lastCurrent = data.currentRMS; - if (mcp->lastCurrent > mcp->maxCurrent) { - mcp->maxCurrent = mcp->lastCurrent; - } - mcp->lastPower = data.activePower; - if (mcp->lastPower > mcp->maxPower) { - mcp->maxPower = mcp->lastPower; - } - mcp->lastReactivePower = data.reactivePower; - mcp->lastApparentPower = data.apparentPower; - mcp->lastPowerFactor = data.powerFactor; - - // Exit and come back immediately if nothing to report - if (!reportHeartbeat) { - // when the reasons for the non-heartbeat report haven't changed, skip - if (!strcmp(reportReasons+1, mcp->lastReasons)) { - return quickly; - } - else if (!mcp->suppressActivityAlarmUntil.hasElapsed()) { - debug.printf("mcp %d: suppressing alarms %s\n", mcp->taskID, reportReasons[1]); - return quickly; - } - } - strlcpy(mcp->lastReasons, reportReasons+1, REPORT_REASONS_LENGTH); - - // Generate a report - J *body = NoteNewBody(); - JAddNumberToObject(body, DATA_FIELD_INSTANCE, mcp->taskID+1); - JAddNumberToObject(body, DATA_FIELD_EVENT_COUNTER, ++eventCounter); - if (reportReasons[0] != '\0') { - JAddStringToObject(body, DATA_FIELD_ALERT, &reportReasons[1]); // [1] skip the first comma - } - JAddNumberToObject(body, DATA_FIELD_VOLTAGE, data.voltageRMS); - JAddNumberToObject(body, DATA_FIELD_CURRENT, data.currentRMS); - JAddNumberToObject(body, DATA_FIELD_POWER, data.activePower); - JAddNumberToObject(body, DATA_FIELD_FREQUENCY, data.lineFrequency); - JAddNumberToObject(body, DATA_FIELD_POWERFACTOR, data.powerFactor); - JAddNumberToObject(body, DATA_FIELD_APPARENT, data.apparentPower); - JAddNumberToObject(body, DATA_FIELD_REACTIVE, data.reactivePower); - JAddStringToObject(body, DATA_FIELD_APP, APP_NAME); - - if (pin.init) { - JAddBoolToObject(body, DATA_FIELD_PIN_ACTIVE, pin.on); - } - - if (vibrationCategory && (envVibrationActiveLine==0 || envVibrationActiveLine==mcp->taskID+1)) { - JAddStringToObject(body, DATA_FIELD_VIBRATION, vibrationCategoryString(vibrationCategory)); - JAddNumberToObject(body, DATA_FIELD_VIBRATION_RAW, vibration); - } - - J *req = notecard.newCommand("note.add"); - JAddStringToObject(req, "file", DATA_FILENAME); - // alerts are always synced immediately. Optionally, all notes can be synced when SYNC_POWER_NOTES is true - if (reportReasons[0] != '\0' || SYNC_POWER_MONITORING_NOTES) { - JAddBoolToObject(req, "sync", true); - } - NoteAddBodyToObject(req, body); - notecard.sendRequest(req); - - // Come back immediately - return quickly; -} - -/** - * @brief Handles a change in activity for a given pin. This supresses alarms for the configured ramp up or ramp down time - * to avoid spurious alarms being sent. - * - * @param active The new activity state. True for active, False for inactive. - * @param pinIndex The MCP that changed activity. - */ -void activityChanged(bool active, int pinIndex) { - mcpContext& mcp = ::mcp[pinIndex]; - if (mcp.taskID) { - float duration = active ? mcp.startup : mcp.shutdown; - mcp.suppressActivityAlarmUntil.set(duration*1000); - } -} - -// Re-load all env vars, returning the modified time -bool refreshEnvironmentVars() -{ - - // Read all env vars from the notecard in one transaction - J *rsp = notecard.requestAndResponse(notecard.newRequest("env.get")); - if (rsp == NULL) { - return false; - } - if (notecard.responseError(rsp)) { - notecard.deleteResponse(rsp); - return false; - } - - // Update the env modified time - environmentModifiedTime = JGetNumber(rsp, "time"); - - // Update the environment - J *body = JGetObject(rsp, "body"); - if (body != NULL) { - updateEnvironment(body); - } - - // Done - notecard.deleteResponse(rsp); - return true; -} - -/** - * @brief Determines if the value of an environment variable corresponds to active, inactive or undefined. - * @param v The environment variable value - * @return int8_t 1 when the value is "1", "on" or "true" - * 0 when the value is "0", "off" or "false" - * -1 for anything else - */ -int8_t parsePinSetting(const char* v) { - if (0 == strcmp(v, "1") || 0 == strcmp(v, "on") || 0 == strcmp(v, "true")) { - return 1; - } - if (0 == strcmp(v, "0") || 0 == strcmp(v, "off") || 0 == strcmp(v, "false")) { - return 0; - } - return -1; -} - -/** - * @brief Updates the direction and state of an activity line from the environment variables. - * - * @param body The JSON object encoding environment variable names and values. - * @param ioPin The pin defintion to update. - */ -bool updatePinFromEnvironment(J *body, pindef& ioPin) -{ - bool changed = false; - const char *output = JGetString(body, ioPin.ovar); - const char *input = JGetString(body, ioPin.ivar); - - int8_t outputSetting = parsePinSetting(output); - int8_t inputSetting = parsePinSetting(input); - - if (outputSetting>=0) { - if (!ioPin.output || !ioPin.init) { - pinMode(ioPin.pin, OUTPUT); - ioPin.output = true; - ioPin.on = !outputSetting; // enure pin state is set below - ioPin.init = true; - changed = true; - } - if (outputSetting != ioPin.on) { // output state has changed - changed = true; - digitalWrite(ioPin.pin, outputSetting==1 ? HIGH : LOW); - ioPin.on = outputSetting; - } - } - else if (inputSetting==1) { - if (ioPin.output || !ioPin.init) { - pinMode(ioPin.pin, INPUT); - ioPin.output = false; - ioPin.init = true; - changed = true; - } - bool oldState = ioPin.on; - updatePinState(ioPin); - changed = oldState != ioPin.on; - } - else { - ioPin.init = false; - pinMode(ioPin.pin, INPUT); - // ideally we would uninitialize the pin here, but for now, set it to input - } - return changed; -} - -/** - * @brief Retrieves the value of an environment variable for a specific line, or when that is not defined, - * the general value. For a given environment variable name, the line variable name is `name_`, while the general value - * is `name`. - * - * @param mcp The line number. - * @param body The body of environment variable data, providing names and values. - * @param name The base name of the environment variable. - * @return const char* The value of the environment variable defined, either for the specific line or more generally for all lines. - */ -const char* getLineEnvironmentVariable(int8_t mcp, J* body, const char* name) { - char varName[strlen(name)+3]; - const char* result = nullptr; - if (mcp>=0) { - sprintf(varName, "%s_%d", name, mcp+1); - result = JGetString(body, varName); - } - if (!result || strlen(result)==0) { - result = JGetString(body, name); - } - return result; -} - -/** - * @brief Updates the environment for a specific monitored instance (line). - * - * @param body The body of environment variable data providing defined names and values. - * @param mcp The instance to update. - */ -void updateMCPEnvironment(J* body, mcpContext& mcp) -{ - const char* alert = getLineEnvironmentVariable(mcp.taskID, body, "alert_power_activity"); - PowerActivityAlert alertType = PowerActivityAlert::NONE; - if (!strcmp(alert, "load")) { - alertType = PowerActivityAlert::LOAD; - } - else if (!strcmp(alert, "supply")) { - alertType = PowerActivityAlert::SUPPLY; - } - mcp.activityAlert = alertType; - - mcp.startup = JAtoN(getLineEnvironmentVariable(mcp.taskID, body, "power_activity_startup_secs"), nullptr); - mcp.shutdown = JAtoN(getLineEnvironmentVariable(mcp.taskID, body, "power_activity_shutdown_secs"), nullptr); -} - - -// Update the environment from the body -void updateEnvironment(J *body) -{ - - // Update heartbeat period - envHeartbeatMins = JAtoN(JGetString(body, "heartbeat_mins"), NULL); - - // Update the voltage monitoring-related env vars - envVoltageUnder = JAtoN(JGetString(body, "alert_under_voltage"), NULL); - envVoltageOver = JAtoN(JGetString(body, "alert_over_voltage"), NULL); - envVoltageChange = JAtoN(JGetString(body, "alert_change_voltage_percent"), NULL); - if (envVoltageUnder == 0.0 && envVoltageOver == 0.0 && envVoltageChange == 0) { - envVoltageChange = 15.0; - } - - // Update the current monitoring-related env vars - envCurrentUnder = JAtoN(JGetString(body, "alert_under_current_amps"), NULL); - envCurrentOver = JAtoN(JGetString(body, "alert_over_current_amps"), NULL); - envCurrentChange = JAtoN(JGetString(body, "alert_change_current_percent"), NULL); - if (envCurrentUnder == 0.0 && envCurrentOver == 0.0 && envCurrentChange == 0) { - envCurrentChange = 15.0; - } - - // Update the power monitoring-related env vars - envPowerUnder = JAtoN(JGetString(body, "alert_under_power_watts"), NULL); - envPowerOver = JAtoN(JGetString(body, "alert_over_power_watts"), NULL); - envPowerChange = JAtoN(JGetString(body, "alert_change_power_percent"), NULL); - if (envPowerUnder == 0.0 && envPowerOver == 0.0 && envPowerChange == 0) { - envPowerChange = 15.0; - } - - envVibrationOff = JAtoN(JGetString(body, "alert_off_vibration"), NULL); - envVibrationUnder = JAtoN(JGetString(body, "alert_under_vibration"), NULL); - envVibrationOver = JAtoN(JGetString(body, "alert_over_vibration"), NULL); - envVibrationActiveLine = JAtoN(JGetString(body, "alert_vibration_activity_line"), NULL); - - // disable vibration activity alerts when there are no vibration bounds configured - if (!isMonitoringVibration()) { - envVibrationActiveLine = 0; - } - - for (int i=0; i #include #include -#include "NoteRTOS.h" +#include "UpbeatLabs_MCP39F521.h" #pragma once @@ -20,35 +20,27 @@ #define SYNC_POWER_MONITORING_NOTES (false) #endif -// Define this when using USB serial, and comment it out when using the ST-Link V3 -// (See USING_SWAN.txt for more info.) -// #define debug Serial - -// Define the debug output stream device, as well as a method enabling us -// to determine whether or not the Serial device is available for app usage. -#ifdef debug -#define serialIsAvailable() false -#else -#define serialIsAvailable() true -#ifdef APP_MAIN -HardwareSerial debug(PG8, PG7); -#else -extern HardwareSerial debug; -#endif -#endif - -// Notecard definition -#ifdef APP_MAIN -Notecard notecard; -#else -extern Notecard notecard; -#endif - -// app.cpp -uint32_t appTasks(uint32_t **taskSchedMs, uint8_t **contextBase, uint32_t *contextSize); -bool appSetup(void); -uint32_t appLoop(void); -bool taskSetup(void *mcp); -uint32_t taskLoop(void *mcp); - -#include "app-name.h" \ No newline at end of file +// Notefile/Note definitions +#define DATA_FILENAME "power.qo" +#define DATA_FIELD_APP "app" +#define DATA_FIELD_MAX_VOLTAGE "max_voltage" +#define DATA_FIELD_MAX_CURRENT "max_current" +#define DATA_FIELD_MAX_POWER "max_power" +#define DATA_FIELD_VOLTAGE "last_voltage" +#define DATA_FIELD_CURRENT "last_current" +#define DATA_FIELD_POWER "last_power" +#define DATA_FIELD_FREQUENCY "frequency" +#define DATA_FIELD_REACTIVE "reactivePower" +#define DATA_FIELD_APPARENT "apparentPower" +#define DATA_FIELD_POWERFACTOR "powerFactor" +#define DATA_FIELD_APP_NAME "nf8" + +#define ALERT_FILENAME "alert.qo" +#define ALERT_FIELD_REASON "reason" + +#define MCP_I2C_ADDRESS_BASE 0x74 + +// Nominal voltage/current for 0. Detection is not totally precise and there is some inherent noise/drift so these are +// set to be just above 0. +#define ZERO_VOLTS (5) +#define ZERO_AMPS (0.3) diff --git a/08-power-quality-monitor/firmware/notepower/metadata.h b/08-power-quality-monitor/firmware/notepower/metadata.h new file mode 100644 index 00000000..21ab187d --- /dev/null +++ b/08-power-quality-monitor/firmware/notepower/metadata.h @@ -0,0 +1,48 @@ +// Definitions used by Notehub for Firmware updates +#include + +#define PRODUCT_ORG_NAME "" +#define PRODUCT_DISPLAY_NAME "Power Quality Monitior" +#define PRODUCT_FIRMWARE_ID "nf8-0.3.0.0" +#define PRODUCT_DESC "" +#define PRODUCT_MAJOR 0 +#define PRODUCT_MINOR 3 +#define PRODUCT_PATCH 0 +#define PRODUCT_BUILD 0 +#define PRODUCT_BUILT __DATE__ " " __TIME__ +#define PRODUCT_BUILDER "" +#define PRODUCT_VERSION STRINGIFY(PRODUCT_MAJOR) "." STRINGIFY(PRODUCT_MINOR) "." STRINGIFY(PRODUCT_PATCH) + +// C trickery to convert a number to a string +#define STRINGIFY(x) STRINGIFY_(x) +#define STRINGIFY_(x) #x + +// This is a product configuration JSON structure that enables the Notehub to recognize this +// firmware when it's uploaded, to help keep track of versions and so we only ever download +// firmware buildss that are appropriate for this device. +#define QUOTE(x) "\"" x "\"" +#define FIRMWARE_VERSION_HEADER "firmware::info:" +#define FIRMWARE_VERSION FIRMWARE_VERSION_HEADER \ + "{" QUOTE("org") ":" QUOTE(PRODUCT_ORG_NAME) \ + "," QUOTE("product") ":" QUOTE(PRODUCT_DISPLAY_NAME) \ + "," QUOTE("description") ":" QUOTE(PRODUCT_DESC) \ + "," QUOTE("firmware") ":" QUOTE(PRODUCT_FIRMWARE_ID) \ + "," QUOTE("version") ":" QUOTE(PRODUCT_VERSION) \ + "," QUOTE("built") ":" QUOTE(PRODUCT_BUILT) \ + "," QUOTE("ver_major") ":" STRINGIFY(PRODUCT_MAJOR) \ + "," QUOTE("ver_minor") ":" STRINGIFY(PRODUCT_MINOR) \ + "," QUOTE("ver_patch") ":" STRINGIFY(PRODUCT_PATCH) \ + "," QUOTE("ver_build") ":" STRINGIFY(PRODUCT_BUILD) \ + "," QUOTE("builder") ":" QUOTE(PRODUCT_BUILDER) \ + "}" + +// In the Arduino IDE, the ino is built regardless of whether or not it is modified. As such, it's a perfect +// place to serve up the build version string because __DATE__ and __TIME__ are updated properly for each build. +const char *productVersion() { + return ("Ver " PRODUCT_VERSION " " PRODUCT_BUILT); +} + +// Return the firmware's version, which is both stored within the image and which is verified by DFU +const char *firmwareVersion() { + return &FIRMWARE_VERSION[strlen(FIRMWARE_VERSION_HEADER)]; +} \ No newline at end of file diff --git a/08-power-quality-monitor/firmware/notepower/notepower.ino b/08-power-quality-monitor/firmware/notepower/notepower.ino index 1849f262..38f6e803 100644 --- a/08-power-quality-monitor/firmware/notepower/notepower.ino +++ b/08-power-quality-monitor/firmware/notepower/notepower.ino @@ -2,79 +2,393 @@ // Use of this source code is governed by licenses granted by the // copyright holder including that found in the LICENSE file. -// App definitions -#define APP_MAIN +#include #include "app.h" +#include "metadata.h" +#include "Notecard.h" +#include "power_monitor.h" +#include "UpbeatLabs_MCP39F521.h" +// For ease of seeing the values, use a program like screen to display +// the Serial output. The serial writes the characters necessary to +// clear the screen on a regular terminal, which means that the serial +// output will stay in place and just update over time. +// In that case, set USING_SCREEN to true +#define USING_SCREEN false -/** - * @brief The main app takes care of setting up the tasks that monitor the Dr Watson instances connected - * to the host. - * - * The code that implements the app is found in `app.cpp`. - */ +#define ENV_POLL_SECS 10 +#define IDLE_UPDATE_PERIOD (1000 * 60 * 5) +#define LIVE_UPDATE_PERIOD (1000 * 60 * 1) -// Task instances -uint32_t tasks = 0; -uint8_t *taskContext; -uint32_t *taskNextRunMs; -uint32_t taskContextSize; +// Variables for Env Var polling +static unsigned long nextPollMs = 0; +static uint32_t lastModifiedTime = 0; +// Variables for sensor reading period when not in live mode +static unsigned long lastUpdateMs; +static unsigned long updatePeriod; + +int led = LED_BUILTIN; + +applicationState state = {0}; +mcpContext mcp = {0}; + +Notecard notecard; +UpbeatLabs_MCP39F521 wattson = UpbeatLabs_MCP39F521(); + +// Forward declarations +bool notecardSetup(void); +void fetchEnvironmentVariables(applicationState &state); +bool pollEnvVars(void); +void sendDataToNotecard(mcpContext *data); +void printMCP39F521Data(UpbeatLabs_MCP39F521_FormattedData *data); -// Arduino entry point void setup() { + pinMode(led, OUTPUT); - // Initialize debug IO - pinMode(LED_BUILTIN, OUTPUT); - while (!debug) ; - debug.begin(115200); - debug.println("*** " __DATE__ " " __TIME__ " ***"); + Serial.begin(115200); // turn on serial communication + delay(250); + Serial.println("Notecard & Dr. Wattson Power Quality Monitoring Example"); + Serial.println("*******************************************************"); + Serial.println("*** " __DATE__ " " __TIME__ " ***"); - // Initialize the RTOS support (see NoteRTOS.h) - _init(); - - // Initialize I2C Wire.begin(); + wattson.begin(); // Pass in the appropriate address, if needed. Defaults to 0x74 // Initialize Notecard library (without doing any I/O on this task) - notecard.setDebugOutputStream(debug); + notecard.setDebugOutputStream(Serial); notecard.begin(); - // Perform setup, including Notefile initialization on the Notecard - while (!appSetup()) { + while (!notecardSetup()) + { delay(750); } - // Discover the instances of the app. By contract, the task context - tasks = appTasks(&taskNextRunMs, &taskContext, &taskContextSize); + updatePeriod = IDLE_UPDATE_PERIOD; - // Setup each individual task - for (uint32_t i=0; i appDueMs || nowMs < prevMs) { - appDueMs = nowMs + appLoop(); + if (pollEnvVars()) + { + fetchEnvironmentVariables(state); } - // Run a single task's 'loop' handler, round-robin - static uint32_t task = 0; - if (task < tasks && (nowMs > taskNextRunMs[task] || nowMs < prevMs)) { - taskNextRunMs[task] = nowMs + taskLoop(&taskContext[task*taskContextSize]); + // Read from MCP + int readMCPretval = wattson.read(&data, NULL); + if (readMCPretval == UpbeatLabs_MCP39F521::SUCCESS) + { + wattson.convertRawData(&data, &fData); + + if (mcp.first) { + mcp.first = false; + mcp.maxVoltage = mcp.lastVoltage = fData.voltageRMS; + mcp.maxCurrent = mcp.lastCurrent = fData.currentRMS; + mcp.maxPower = mcp.lastPower = fData.activePower; + } + + // Check for Alarms based on last readings + if (state.envVoltageUnder != 0 && fData.voltageRMS < state.envVoltageUnder) { + sendAlert("Under voltage detected!"); + } + if (state.envVoltageOver != 0 && fData.voltageRMS > state.envVoltageOver) { + sendAlert("Over voltage detected!"); + } + + float d = mcp.maxVoltage * (state.envVoltageChange * 0.01) < 1 ? 1 : mcp.maxVoltage * (state.envVoltageChange * 0.01); + if (state.envVoltageChange != 0 && ((fData.voltageRMS < mcp.lastVoltage-d) || fData.voltageRMS > mcp.lastVoltage+d)) { + sendAlert("Voltage changed outside of set threshold!"); + } + if (fData.voltageRMS <= ZERO_VOLTS) { + sendAlert("No voltage detected!"); + } + + if (state.envCurrentUnder != 0 && fData.currentRMS < state.envCurrentUnder) { + sendAlert("Under current detected!"); + } + if (state.envCurrentOver != 0 && fData.currentRMS > state.envCurrentOver) { + sendAlert("Over current detected!"); + } + d = mcp.maxCurrent * (state.envCurrentChange * 0.01) < 1 ? 1 : mcp.maxCurrent * (state.envCurrentChange * 0.01); + if (state.envCurrentChange != 0 && ((fData.currentRMS < mcp.lastCurrent-d) || fData.currentRMS > mcp.lastCurrent+d)) { + sendAlert("Current changed outside of set threshold!"); + } + + if (state.envSendPowerAlarms) { + if (state.envPowerUnder != 0 && fData.activePower < state.envPowerUnder) { + sendAlert("Under power detected!"); + } + if (state.envPowerOver != 0 && fData.activePower > state.envPowerOver) { + sendAlert("Over power detected!"); + } + + d = mcp.maxPower * (state.envPowerChange * 0.01) < 1 ? 1 : mcp.maxPower * (state.envPowerChange * 0.01); + if (state.envPowerChange != 0 && ((fData.activePower < mcp.lastPower-d) || fData.activePower > mcp.lastPower+d)) { + sendAlert("Power changed outside of set threshold!"); + } + if (fData.currentRMS <= ZERO_AMPS) { + sendAlert("No power detected!"); + } + } + + // Update mcpContext with latest readings + mcp.lastVoltage = fData.voltageRMS; + if (mcp.lastVoltage > mcp.maxVoltage) { + mcp.maxVoltage = mcp.lastVoltage; + } + mcp.lastCurrent = fData.currentRMS; + if (mcp.lastCurrent > mcp.maxCurrent) { + mcp.maxCurrent = mcp.lastCurrent; + } + mcp.lastPower = fData.activePower; + if (mcp.lastPower > mcp.maxPower) { + mcp.maxPower = mcp.lastPower; + } + mcp.lastReactivePower = fData.reactivePower; + mcp.lastApparentPower = fData.apparentPower; + mcp.lastPowerFactor = fData.powerFactor; + mcp.lastFrequency = fData.lineFrequency; + + // Send Heartbeat messages + const uint32_t currentMillis = millis(); + if (currentMillis - lastUpdateMs >= updatePeriod) + { + printMCP39F521Data(&fData); + // Change to use mcpContext + sendDataToNotecard(&mcp); + lastUpdateMs = currentMillis; + } } - task = task+1 >= tasks ? 0 : task+1; + else + { + Serial.print("Error returned! "); + Serial.println(readMCPretval); + } +} + +void sendDataToNotecard(mcpContext *data) +{ + J *body = NoteNewBody(); + JAddNumberToObject(body, DATA_FIELD_VOLTAGE, data->lastVoltage); + JAddNumberToObject(body, DATA_FIELD_CURRENT, data->lastCurrent); + JAddNumberToObject(body, DATA_FIELD_POWER, data->lastPower); + JAddNumberToObject(body, DATA_FIELD_MAX_VOLTAGE, data->maxVoltage); + JAddNumberToObject(body, DATA_FIELD_MAX_CURRENT, data->maxCurrent); + JAddNumberToObject(body, DATA_FIELD_MAX_POWER, data->maxPower); + JAddNumberToObject(body, DATA_FIELD_FREQUENCY, data->lastFrequency); + JAddNumberToObject(body, DATA_FIELD_POWERFACTOR, data->lastPowerFactor); + JAddNumberToObject(body, DATA_FIELD_APPARENT, data->lastApparentPower); + JAddNumberToObject(body, DATA_FIELD_REACTIVE, data->lastReactivePower); + JAddStringToObject(body, DATA_FIELD_APP, DATA_FIELD_APP_NAME); + + J *req = notecard.newCommand("note.add"); + JAddStringToObject(req, "file", DATA_FILENAME); + // JAddBoolToObject(req, "sync", true); + NoteAddBodyToObject(req, body); + notecard.sendRequest(req); +} + +void sendAlert(const char *reason) +{ + J *body = NoteNewBody(); + JAddStringToObject(body, ALERT_FIELD_REASON, reason); + + J *req = notecard.newCommand("note.add"); + JAddStringToObject(req, "file", ALERT_FILENAME); + JAddBoolToObject(req, "sync", true); + NoteAddBodyToObject(req, body); + notecard.sendRequest(req); +} + +void printMCP39F521Data(UpbeatLabs_MCP39F521_FormattedData *data) +{ + Serial.println(); + Serial.print("New reading @ "); + Serial.println(millis()); + Serial.print(F("Voltage = ")); + Serial.println(data->voltageRMS, 4); + Serial.print(F("Current (Amps) = ")); + Serial.println(data->currentRMS, 4); + Serial.print(F("Line Frequency (Hz) = ")); + Serial.println(data->lineFrequency, 4); + Serial.print("Analog Input Voltage = "); + Serial.println(data->analogInputVoltage, 4); + Serial.print(F("Power Factor (active / apparent) = ")); + Serial.println(data->powerFactor, 4); + Serial.print(F("Active Power (Watts) = ")); + Serial.println(data->activePower, 4); + Serial.print(F("Reactive Power (VAR) = ")); + Serial.println(data->reactivePower, 4); + Serial.print(F("Apparent Power (VA) = ")); + Serial.println(data->apparentPower, 4); + Serial.println(); +} + +// Set up the Notecard in preparation for the mcp task +bool notecardSetup(void) +{ + // Initialize the Notecard for I2C + J *req = notecard.newRequest("hub.set"); + JAddStringToObject(req, "product", PRODUCT_UID); + JAddStringToObject(req, "mode", "periodic"); + JAddNumberToObject(req, "inbound", 60*24); + JAddNumberToObject(req, "outbound", 60); + if (!notecard.sendRequest(req)) { + Serial.printf("notecard not responding\n"); + return false; + } + + // Add the notefile template + J *body = JCreateObject(); + JAddStringToObject(body, DATA_FIELD_APP, TSTRINGV); + JAddNumberToObject(body, DATA_FIELD_VOLTAGE, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_CURRENT, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_POWER, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_MAX_VOLTAGE, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_MAX_CURRENT, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_MAX_POWER, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_FREQUENCY, TFLOAT16); + JAddNumberToObject(body, DATA_FIELD_APPARENT, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_REACTIVE, TFLOAT32); + JAddNumberToObject(body, DATA_FIELD_POWERFACTOR, TFLOAT16); + + req = notecard.newCommand("note.template"); + JAddStringToObject(req, "file", DATA_FILENAME); + JAddItemToObject(req, "body", body); + notecard.sendRequest(req); + + // Notify Notehub of the current firmware version + req = notecard.newRequest("dfu.status"); + if (req != NULL) + { + JAddStringToObject(req, "version", firmwareVersion()); + notecard.sendRequest(req); + } + + // Enable Outboard DFU + req = notecard.newRequest("card.dfu"); + if (req != NULL) + { + JAddStringToObject(req, "name", "stm32"); + JAddBoolToObject(req, "on", true); + notecard.sendRequest(req); + } + + // Done + return true; + +} + +void fetchEnvironmentVariables(applicationState &vars) +{ + J *req = notecard.newRequest("env.get"); + + J *names = JAddArrayToObject(req, "names"); + JAddItemToArray(names, JCreateString("heartbeat_mins")); + JAddItemToArray(names, JCreateString("alert_under_voltage")); + JAddItemToArray(names, JCreateString("alert_over_voltage")); + JAddItemToArray(names, JCreateString("alert_change_voltage_percent")); + JAddItemToArray(names, JCreateString("alert_under_current_amps")); + JAddItemToArray(names, JCreateString("alert_over_current_amps")); + JAddItemToArray(names, JCreateString("alert_change_current_percent")); + JAddItemToArray(names, JCreateString("send_power_alarms")); + JAddItemToArray(names, JCreateString("alert_under_power_watts")); + JAddItemToArray(names, JCreateString("alert_over_power_watts")); + JAddItemToArray(names, JCreateString("alert_change_power_percent")); + + J *rsp = notecard.requestAndResponse(req); + if (rsp != NULL) + { + if (notecard.responseError(rsp)) + { + notecard.deleteResponse(rsp); + return; + } + + // Get the note's body + J *body = JGetObject(rsp, "body"); + if (body != NULL) + { + // Update heartbeat period + vars.envHeartbeatMins = JAtoN(JGetString(body, "heartbeat_mins"), NULL); + + // Update the voltage monitoring-related env vars + vars.envVoltageUnder = JAtoN(JGetString(body, "alert_under_voltage"), NULL); + vars.envVoltageOver = JAtoN(JGetString(body, "alert_over_voltage"), NULL); + vars.envVoltageChange = JAtoN(JGetString(body, "alert_change_voltage_percent"), NULL); + if (vars.envVoltageUnder == 0.0 && vars.envVoltageOver == 0.0 && vars.envVoltageChange == 0) { + vars.envVoltageChange = 15.0; + } + + // Update the current monitoring-related env vars + vars.envCurrentUnder = JAtoN(JGetString(body, "alert_under_current_amps"), NULL); + vars.envCurrentOver = JAtoN(JGetString(body, "alert_over_current_amps"), NULL); + vars.envCurrentChange = JAtoN(JGetString(body, "alert_change_current_percent"), NULL); + if (vars.envCurrentUnder == 0.0 && vars.envCurrentOver == 0.0 && vars.envCurrentChange == 0) { + vars.envCurrentChange = 15.0; + } + + // Update the power monitoring-related env vars + vars.envPowerUnder = JAtoN(JGetString(body, "alert_under_power_watts"), NULL); + vars.envPowerOver = JAtoN(JGetString(body, "alert_over_power_watts"), NULL); + vars.envPowerChange = JAtoN(JGetString(body, "alert_change_power_percent"), NULL); + if (vars.envPowerUnder == 0.0 && vars.envPowerOver == 0.0 && vars.envPowerChange == 0) { + vars.envPowerChange = 15.0; + } + + char *sendAlarms = JGetString(body, "send_power_alarms"); + vars.envSendPowerAlarms = (strcmp(sendAlarms, "true") == 0 || strcmp(sendAlarms, "1") == 0); + + updatePeriod = vars.envHeartbeatMins > 0 ? (vars.envHeartbeatMins * 1000 * 60) : IDLE_UPDATE_PERIOD; + } + } + notecard.deleteResponse(rsp); +} + +bool pollEnvVars() +{ + if (millis() < nextPollMs) + { + return false; + } + + nextPollMs = millis() + (ENV_POLL_SECS * 1000); + + J *rsp = notecard.requestAndResponse(notecard.newRequest("env.modified")); + + if (rsp == NULL) + { + return false; + } + + uint32_t modifiedTime = JGetInt(rsp, "time"); + notecard.deleteResponse(rsp); + if (lastModifiedTime == modifiedTime) + { + return false; + } + + lastModifiedTime = modifiedTime; - // Handle system timer wrap - prevMs = nowMs; + return true; } diff --git a/08-power-quality-monitor/firmware/notepower/power_monitor.h b/08-power-quality-monitor/firmware/notepower/power_monitor.h new file mode 100644 index 00000000..206d90d2 --- /dev/null +++ b/08-power-quality-monitor/firmware/notepower/power_monitor.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "Notecard.h" + +typedef struct { + uint8_t taskID; + float lastApparentPower; + float lastReactivePower; + float lastPowerFactor; + float lastVoltage; + float lastCurrent; + float lastPower; + float lastFrequency; + float maxVoltage; + float maxCurrent; + float maxPower; + float startup; // duration in seconds + float shutdown; // duration in seconds + bool first; +} mcpContext; + +typedef struct { + uint32_t envHeartbeatMins; + float envVoltageUnder; + float envVoltageOver; + float envVoltageChange; + float envCurrentUnder; + float envCurrentOver; + float envCurrentChange; + bool envSendPowerAlarms; + float envPowerUnder; + float envPowerOver; + float envPowerChange; +} applicationState; diff --git a/08-power-quality-monitor/firmware/platformio.ini b/08-power-quality-monitor/firmware/platformio.ini index 8232c01f..ec46126d 100644 --- a/08-power-quality-monitor/firmware/platformio.ini +++ b/08-power-quality-monitor/firmware/platformio.ini @@ -17,11 +17,11 @@ platform = ststm32 board = bw_swan_r5 framework = arduino debug_tool = stlink -build_flags = +build_flags = # uncomment this to enable USB Serial -# -D PIO_FRAMEWORK_ARDUINO_ENABLE_CDC + -D PIO_FRAMEWORK_ARDUINO_ENABLE_CDC -D SERIAL_RX_BUFFER_SIZE=4096 -debug_init_break = -lib_deps = +debug_init_break = +lib_deps = blues/Blues Wireless Notecard@^1.3.13 upbeatlabs/UpbeatLabs MCP39F521@^2.0.0 From 568b6a65c360ff35a9143e3b672d44d9ae3152f1 Mon Sep 17 00:00:00 2001 From: Brandon Satrom Date: Mon, 12 Jun 2023 16:58:20 -0500 Subject: [PATCH 2/2] feat: refactor firmware; add streamlit dashboard --- 08-power-quality-monitor/dashboard/.gitignore | 131 +++ 08-power-quality-monitor/dashboard/Pipfile | 17 + .../dashboard/Pipfile.lock | 1045 +++++++++++++++++ .../dashboard/Power_Monitor.py | 110 ++ .../dashboard/pages/About.py | 18 + .../dashboard/requirements.txt | 5 + .../firmware/notepower/metadata.h | 4 +- .../firmware/notepower/notepower.ino | 30 +- .../firmware/notepower/power_monitor.h | 2 + 9 files changed, 1356 insertions(+), 6 deletions(-) create mode 100644 08-power-quality-monitor/dashboard/.gitignore create mode 100644 08-power-quality-monitor/dashboard/Pipfile create mode 100644 08-power-quality-monitor/dashboard/Pipfile.lock create mode 100644 08-power-quality-monitor/dashboard/Power_Monitor.py create mode 100644 08-power-quality-monitor/dashboard/pages/About.py create mode 100644 08-power-quality-monitor/dashboard/requirements.txt diff --git a/08-power-quality-monitor/dashboard/.gitignore b/08-power-quality-monitor/dashboard/.gitignore new file mode 100644 index 00000000..34cf1a5d --- /dev/null +++ b/08-power-quality-monitor/dashboard/.gitignore @@ -0,0 +1,131 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +secrets.toml \ No newline at end of file diff --git a/08-power-quality-monitor/dashboard/Pipfile b/08-power-quality-monitor/dashboard/Pipfile new file mode 100644 index 00000000..ce2ef521 --- /dev/null +++ b/08-power-quality-monitor/dashboard/Pipfile @@ -0,0 +1,17 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +altair = "*" +pandas = "*" +streamlit = "*" +snowflake-connector-python = "==2.7.6" +matplotlib = "*" +streamlit-option-menu = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/08-power-quality-monitor/dashboard/Pipfile.lock b/08-power-quality-monitor/dashboard/Pipfile.lock new file mode 100644 index 00000000..40cc02cc --- /dev/null +++ b/08-power-quality-monitor/dashboard/Pipfile.lock @@ -0,0 +1,1045 @@ +{ + "_meta": { + "hash": { + "sha256": "85b073f37410ed876b8240922e49f57938f2dff7eaf9153cb346f4b1c3cf7b03" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "altair": { + "hashes": [ + "sha256:39399a267c49b30d102c10411e67ab26374156a84b1aeb9fcd15140429ba49c5", + "sha256:8b45ebeaf8557f2d760c5c77b79f02ae12aee7c46c27c06014febab6f849bc87" + ], + "index": "pypi", + "version": "==4.2.2" + }, + "asn1crypto": { + "hashes": [ + "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", + "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67" + ], + "version": "==1.5.1" + }, + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "blinker": { + "hashes": [ + "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213", + "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.6.2" + }, + "cachetools": { + "hashes": [ + "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14", + "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4" + ], + "markers": "python_version ~= '3.7'", + "version": "==5.3.0" + }, + "certifi": { + "hashes": [ + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.5.7" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + ], + "markers": "python_full_version >= '3.5.0'", + "version": "==2.0.12" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, + "contourpy": { + "hashes": [ + "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98", + "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772", + "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2", + "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc", + "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803", + "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051", + "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc", + "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4", + "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436", + "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5", + "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5", + "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3", + "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80", + "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1", + "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0", + "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae", + "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556", + "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02", + "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566", + "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350", + "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967", + "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4", + "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66", + "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69", + "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd", + "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2", + "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810", + "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50", + "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc", + "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2", + "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0", + "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3", + "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6", + "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac", + "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d", + "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6", + "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f", + "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd", + "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566", + "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa", + "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414", + "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a", + "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c", + "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693", + "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d", + "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161", + "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e", + "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2", + "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f", + "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71", + "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd", + "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9", + "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8", + "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab", + "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.7" + }, + "cryptography": { + "hashes": [ + "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b", + "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51", + "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7", + "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d", + "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6", + "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29", + "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9", + "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf", + "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815", + "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf", + "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85", + "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77", + "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86", + "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb", + "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e", + "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0", + "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3", + "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84", + "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", + "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" + ], + "markers": "python_version >= '3.6'", + "version": "==36.0.2" + }, + "cycler": { + "hashes": [ + "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3", + "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f" + ], + "markers": "python_version >= '3.6'", + "version": "==0.11.0" + }, + "decorator": { + "hashes": [ + "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", + "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" + ], + "markers": "python_version >= '3.5'", + "version": "==5.1.1" + }, + "entrypoints": { + "hashes": [ + "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", + "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f" + ], + "markers": "python_version >= '3.6'", + "version": "==0.4" + }, + "fonttools": { + "hashes": [ + "sha256:106caf6167c4597556b31a8d9175a3fdc0356fdcd70ab19973c3b0d4c893c461", + "sha256:dba8d7cdb8e2bac1b3da28c5ed5960de09e59a2fe7e63bb73f5a59e57b0430d2" + ], + "markers": "python_version >= '3.8'", + "version": "==4.39.4" + }, + "gitdb": { + "hashes": [ + "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a", + "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7" + ], + "markers": "python_version >= '3.7'", + "version": "==4.0.10" + }, + "gitpython": { + "hashes": [ + "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573", + "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.31" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "importlib-metadata": { + "hashes": [ + "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed", + "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" + ], + "markers": "python_version >= '3.7'", + "version": "==6.6.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6", + "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a" + ], + "markers": "python_version < '3.10'", + "version": "==5.12.0" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, + "kiwisolver": { + "hashes": [ + "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", + "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", + "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", + "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", + "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", + "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4", + "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9", + "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286", + "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767", + "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", + "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", + "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b", + "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", + "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", + "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494", + "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", + "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626", + "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", + "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514", + "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", + "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f", + "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", + "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", + "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", + "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51", + "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", + "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8", + "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", + "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb", + "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da", + "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", + "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", + "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", + "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", + "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008", + "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", + "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", + "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", + "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", + "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9", + "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", + "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", + "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", + "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", + "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2", + "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5", + "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69", + "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", + "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", + "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", + "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e", + "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", + "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca", + "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", + "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", + "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", + "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4", + "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6", + "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686", + "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", + "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", + "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", + "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750", + "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", + "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", + "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2", + "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", + "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" + ], + "markers": "python_version >= '3.7'", + "version": "==1.4.4" + }, + "markdown-it-py": { + "hashes": [ + "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", + "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.2.0" + }, + "markupsafe": { + "hashes": [ + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "matplotlib": { + "hashes": [ + "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353", + "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1", + "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290", + "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7", + "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba", + "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717", + "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96", + "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136", + "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba", + "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613", + "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc", + "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de", + "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b", + "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500", + "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea", + "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042", + "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa", + "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb", + "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d", + "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61", + "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882", + "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0", + "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b", + "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6", + "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc", + "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332", + "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439", + "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c", + "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1", + "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529", + "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0", + "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb", + "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7", + "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24", + "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7", + "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87", + "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4", + "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556", + "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476", + "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb", + "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304" + ], + "index": "pypi", + "version": "==3.7.1" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "numpy": { + "hashes": [ + "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187", + "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812", + "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7", + "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4", + "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6", + "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0", + "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4", + "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570", + "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4", + "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f", + "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80", + "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289", + "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385", + "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078", + "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c", + "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463", + "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3", + "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950", + "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155", + "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7", + "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c", + "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096", + "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17", + "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf", + "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4", + "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02", + "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c", + "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b" + ], + "markers": "python_version >= '3.8'", + "version": "==1.24.3" + }, + "oscrypto": { + "hashes": [ + "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", + "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4" + ], + "version": "==1.3.0" + }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pandas": { + "hashes": [ + "sha256:00959a04a1d7bbc63d75a768540fb20ecc9e65fd80744c930e23768345a362a7", + "sha256:03e677c6bc9cfb7f93a8b617d44f6091613a5671ef2944818469be7b42114a00", + "sha256:0a514ae436b23a92366fbad8365807fc0eed15ca219690b3445dcfa33597a5cc", + "sha256:12bd6618e3cc737c5200ecabbbb5eaba8ab645a4b0db508ceeb4004bb10b060e", + "sha256:18d22cb9043b6c6804529810f492ab09d638ddf625c5dea8529239607295cb59", + "sha256:19b8e5270da32b41ebf12f0e7165efa7024492e9513fb46fb631c5022ae5709d", + "sha256:2b6fe5f7ce1cba0e74188c8473c9091ead9b293ef0a6794939f8cc7947057abd", + "sha256:320b180d125c3842c5da5889183b9a43da4ebba375ab2ef938f57bf267a3c684", + "sha256:3d099ecaa5b9e977b55cd43cf842ec13b14afa1cfa51b7e1179d90b38c53ce6a", + "sha256:6c0853d487b6c868bf107a4b270a823746175b1932093b537b9b76c639fc6f7e", + "sha256:6fa0067f2419f933101bdc6001bcea1d50812afbd367b30943417d67fbb99678", + "sha256:70a996a1d2432dadedbb638fe7d921c88b0cc4dd90374eab51bb33dc6c0c2a12", + "sha256:7b8395d335b08bc8b050590da264f94a439b4770ff16bb51798527f1dd840388", + "sha256:7bbf173d364130334e0159a9a034f573e8b44a05320995127cf676b85fd8ce86", + "sha256:8db5a644d184a38e6ed40feeb12d410d7fcc36648443defe4707022da127fc35", + "sha256:909a72b52175590debbf1d0c9e3e6bce2f1833c80c76d80bd1aa09188be768e5", + "sha256:90d1d365d77d287063c5e339f49b27bd99ef06d10a8843cf00b1a49326d492c1", + "sha256:910df06feaf9935d05247db6de452f6d59820e432c18a2919a92ffcd98f8f79b", + "sha256:99f7192d8b0e6daf8e0d0fd93baa40056684e4b4aaaef9ea78dff34168e1f2f0", + "sha256:a2564629b3a47b6aa303e024e3d84e850d36746f7e804347f64229f8c87416ea", + "sha256:a37ee35a3eb6ce523b2c064af6286c45ea1c7ff882d46e10d0945dbda7572753", + "sha256:af2449e9e984dfad39276b885271ba31c5e0204ffd9f21f287a245980b0e4091", + "sha256:e09a53a4fe8d6ae2149959a2d02e1ef2f4d2ceb285ac48f74b79798507e468b4", + "sha256:f25e23a03f7ad7211ffa30cb181c3e5f6d96a8e4cb22898af462a7333f8a74eb", + "sha256:fe7914d8ddb2d54b900cec264c090b88d141a1eed605c9539a187dbc2547f022" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "pillow": { + "hashes": [ + "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1", + "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba", + "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a", + "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799", + "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51", + "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb", + "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5", + "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270", + "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6", + "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47", + "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf", + "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e", + "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b", + "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66", + "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865", + "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec", + "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c", + "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1", + "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38", + "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906", + "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705", + "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef", + "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc", + "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f", + "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf", + "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392", + "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d", + "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe", + "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32", + "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5", + "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7", + "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44", + "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d", + "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3", + "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625", + "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e", + "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829", + "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089", + "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3", + "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78", + "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96", + "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964", + "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597", + "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99", + "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a", + "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140", + "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7", + "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16", + "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903", + "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1", + "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296", + "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572", + "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115", + "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a", + "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd", + "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4", + "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1", + "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb", + "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa", + "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a", + "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569", + "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c", + "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf", + "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082", + "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062", + "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579" + ], + "markers": "python_version >= '3.7'", + "version": "==9.5.0" + }, + "protobuf": { + "hashes": [ + "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7", + "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c", + "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2", + "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b", + "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050", + "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9", + "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7", + "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454", + "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480", + "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469", + "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c", + "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e", + "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db", + "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905", + "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b", + "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86", + "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4", + "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402", + "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7", + "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4", + "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99", + "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee" + ], + "markers": "python_version >= '3.7'", + "version": "==3.20.3" + }, + "pyarrow": { + "hashes": [ + "sha256:0846ace49998825eda4722f8d7f83fa05601c832549c9087ea49d6d5397d8cec", + "sha256:0d8b90efc290e99a81d06015f3a46601c259ecc81ffb6d8ce288c91bd1b868c9", + "sha256:0e36425b1c1cbf5447718b3f1751bf86c58f2b3ad299f996cd9b1aa040967656", + "sha256:19c812d303610ab5d664b7b1de4051ae23565f9f94d04cbea9e50569746ae1ee", + "sha256:1b50bb9a82dca38a002d7cbd802a16b1af0f8c50ed2ec94a319f5f2afc047ee9", + "sha256:1d568acfca3faa565d663e53ee34173be8e23a95f78f2abfdad198010ec8f745", + "sha256:23a77d97f4d101ddfe81b9c2ee03a177f0e590a7e68af15eafa06e8f3cf05976", + "sha256:2466be046b81863be24db370dffd30a2e7894b4f9823fb60ef0a733c31ac6256", + "sha256:272f147d4f8387bec95f17bb58dcfc7bc7278bb93e01cb7b08a0e93a8921e18e", + "sha256:280289ebfd4ac3570f6b776515baa01e4dcbf17122c401e4b7170a27c4be63fd", + "sha256:2cc63e746221cddb9001f7281dee95fd658085dd5b717b076950e1ccc607059c", + "sha256:3b97649c8a9a09e1d8dc76513054f1331bd9ece78ee39365e6bf6bc7503c1e94", + "sha256:3d1733b1ea086b3c101427d0e57e2be3eb964686e83c2363862a887bb5c41fa8", + "sha256:5b0810864a593b89877120972d1f7af1d1c9389876dbed92b962ed81492d3ffc", + "sha256:7a7b6a765ee4f88efd7d8348d9a1f804487d60799d0428b6ddf3344eaef37282", + "sha256:7b5b9f60d9ef756db59bec8d90e4576b7df57861e6a3d6a8bf99538f68ca15b3", + "sha256:92fb031e6777847f5c9b01eaa5aa0c9033e853ee80117dce895f116d8b0c3ca3", + "sha256:993287136369aca60005ee7d64130f9466489c4f7425f5c284315b0a5401ccd9", + "sha256:a1c4fce253d5bdc8d62f11cfa3da5b0b34b562c04ce84abb8bd7447e63c2b327", + "sha256:a7cd32fe77f967fe08228bc100433273020e58dd6caced12627bcc0a7675a513", + "sha256:b99e559d27db36ad3a33868a475f03e3129430fc065accc839ef4daa12c6dab6", + "sha256:bc4ea634dacb03936f50fcf59574a8e727f90c17c24527e488d8ceb52ae284de", + "sha256:d8c26912607e26c2991826bbaf3cf2b9c8c3e17566598c193b492f058b40d3a4", + "sha256:e6be4d85707fc8e7a221c8ab86a40449ce62559ce25c94321df7c8500245888f", + "sha256:ea830d9f66bfb82d30b5794642f83dd0e4a718846462d22328981e9eb149cba8" + ], + "markers": "python_version >= '3.7'", + "version": "==12.0.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pycryptodomex": { + "hashes": [ + "sha256:0af93aad8d62e810247beedef0261c148790c52f3cd33643791cc6396dd217c1", + "sha256:12056c38e49d972f9c553a3d598425f8a1c1d35b2e4330f89d5ff1ffb70de041", + "sha256:23d83b610bd97704f0cd3acc48d99b76a15c8c1540d8665c94d514a49905bad7", + "sha256:2d4d395f109faba34067a08de36304e846c791808524614c731431ee048fe70a", + "sha256:32e764322e902bbfac49ca1446604d2839381bbbdd5a57920c9daaf2e0b778df", + "sha256:3c2516b42437ae6c7a29ef3ddc73c8d4714e7b6df995b76be4695bbe4b3b5cd2", + "sha256:40e8a11f578bd0851b02719c862d55d3ee18d906c8b68a9c09f8c564d6bb5b92", + "sha256:4b51e826f0a04d832eda0790bbd0665d9bfe73e5a4d8ea93b6a9b38beeebe935", + "sha256:4c4674f4b040321055c596aac926d12f7f6859dfe98cd12f4d9453b43ab6adc8", + "sha256:55eed98b4150a744920597c81b3965b632038781bab8a08a12ea1d004213c600", + "sha256:599bb4ae4bbd614ca05f49bd4e672b7a250b80b13ae1238f05fd0f09d87ed80a", + "sha256:5c23482860302d0d9883404eaaa54b0615eefa5274f70529703e2c43cc571827", + "sha256:64b876d57cb894b31056ad8dd6a6ae1099b117ae07a3d39707221133490e5715", + "sha256:67a3648025e4ddb72d43addab764336ba2e670c8377dba5dd752e42285440d31", + "sha256:6feedf4b0e36b395329b4186a805f60f900129cdf0170e120ecabbfcb763995d", + "sha256:78f0ddd4adc64baa39b416f3637aaf99f45acb0bcdc16706f0cc7ebfc6f10109", + "sha256:7a6651a07f67c28b6e978d63aa3a3fccea0feefed9a8453af3f7421a758461b7", + "sha256:7a8dc3ee7a99aae202a4db52de5a08aa4d01831eb403c4d21da04ec2f79810db", + "sha256:7cc28dd33f1f3662d6da28ead4f9891035f63f49d30267d3b41194c8778997c8", + "sha256:7fa0b52df90343fafe319257b31d909be1d2e8852277fb0376ba89d26d2921db", + "sha256:88b0d5bb87eaf2a31e8a759302b89cf30c97f2f8ca7d83b8c9208abe8acb447a", + "sha256:a4fa037078e92c7cc49f6789a8bac3de06856740bb2038d05f2d9a2e4b165d59", + "sha256:a57e3257bacd719769110f1f70dd901c5b6955e9596ad403af11a3e6e7e3311c", + "sha256:ab33c2d9f275e05e235dbca1063753b5346af4a5cac34a51fa0da0d4edfb21d7", + "sha256:c84689c73358dfc23f9fdcff2cb9e7856e65e2ce3b5ed8ff630d4c9bdeb1867b", + "sha256:c92537b596bd5bffb82f8964cabb9fef1bca8a28a9e0a69ffd3ec92a4a7ad41b", + "sha256:caa937ff29d07a665dfcfd7a84f0d4207b2ebf483362fa9054041d67fdfacc20", + "sha256:d38ab9e53b1c09608ba2d9b8b888f1e75d6f66e2787e437adb1fecbffec6b112", + "sha256:d4cf0128da167562c49b0e034f09e9cedd733997354f2314837c2fa461c87bb1", + "sha256:db23d7341e21b273d2440ec6faf6c8b1ca95c8894da612e165be0b89a8688340", + "sha256:ee8bf4fdcad7d66beb744957db8717afc12d176e3fd9c5d106835133881a049b", + "sha256:f854c8476512cebe6a8681cc4789e4fcff6019c17baa0fd72b459155dc605ab4", + "sha256:fd29d35ac80755e5c0a99d96b44fb9abbd7e871849581ea6a4cb826d24267537" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.17" + }, + "pydeck": { + "hashes": [ + "sha256:9e0a67890ab061b8c6080e06f8c780934c00355a7114291c884f055f3fc0dc25", + "sha256:c89b3dd76f9991140a33b886b336c762105e9c9def8e842e891bc72dbce8a4ce" + ], + "markers": "python_version >= '3.7'", + "version": "==0.8.1b0" + }, + "pygments": { + "hashes": [ + "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", + "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.15.1" + }, + "pyjwt": { + "hashes": [ + "sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1", + "sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074" + ], + "markers": "python_version >= '3.7'", + "version": "==2.7.0" + }, + "pympler": { + "hashes": [ + "sha256:993f1a3599ca3f4fcd7160c7545ad06310c9e12f70174ae7ae8d4e25f6c5d3fa", + "sha256:d260dda9ae781e1eab6ea15bacb84015849833ba5555f141d2d9b7b7473b307d" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.1" + }, + "pyopenssl": { + "hashes": [ + "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3", + "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==21.0.0" + }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, + "pyrsistent": { + "hashes": [ + "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", + "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", + "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", + "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", + "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", + "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", + "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", + "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", + "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", + "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", + "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", + "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", + "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", + "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", + "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", + "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", + "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", + "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", + "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", + "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", + "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", + "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", + "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", + "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", + "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", + "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", + "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" + ], + "markers": "python_version >= '3.7'", + "version": "==0.19.3" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "pytz": { + "hashes": [ + "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", + "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb" + ], + "version": "==2023.3" + }, + "pytz-deprecation-shim": { + "hashes": [ + "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6", + "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.1.0.post0" + }, + "requests": { + "hashes": [ + "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294", + "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4" + ], + "markers": "python_version >= '3.7'", + "version": "==2.30.0" + }, + "rich": { + "hashes": [ + "sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c", + "sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==13.3.5" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.7'", + "version": "==67.7.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "smmap": { + "hashes": [ + "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", + "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" + ], + "markers": "python_version >= '3.6'", + "version": "==5.0.0" + }, + "snowflake-connector-python": { + "hashes": [ + "sha256:22437ee26baaa83cabcebbe9fbc9c95b36bdca441db61822588a622e32bcbecd", + "sha256:331628e0ca02764492b83b5e47703bd857115ae29346b72de32d7fb7c9ec548b", + "sha256:4aa90cddbc46f8f580ad78cbb9d4741fd66e665d3223f7f907d859554d2025c2", + "sha256:5f3faa237f22d8305c8e48d8b5110c4015485b2cfa683e176fa89eafe57f6a8e", + "sha256:74f55a0d25a53378994939b43a2a55105f12b1b92c8b7cb863b490c9a4243147", + "sha256:781473bd60388ce651ad094288f231bcae7fa5cd447e64d12577736cb37b8c96", + "sha256:8c82eda884967ef00d1b7aa59c925c83368f886bfa447c8fa2a81dced03dea70", + "sha256:8cfce34bbb910a396b230768658ac65578f28073b62cc9c85289ebbd217e839d", + "sha256:8dbaf2dfed6c7ba4fd90c9f0ec604f86a2e070e4219c182f79f44ca225749c18", + "sha256:d94eea3047c6cf6d5e4bb8a4b9f961f77f87e25671e7cb6257c0459419a9b194", + "sha256:e180d285e93d5e3fe2b7b4958212d4964d45f1eca94f03dedc4e569bcc550bfd", + "sha256:ea850b2d816263ae63c00ad60f4df7006ddb1854f28aaa5b3688bf0e5e458dca", + "sha256:f59ed0c3223548a25aca9bcacf2c5cde04ad5e15366f0cf9e7b20dc34554b1cf" + ], + "index": "pypi", + "version": "==2.7.6" + }, + "streamlit": { + "hashes": [ + "sha256:520dd9b9e6efb559b5a9a22feadb48b1e6f0340ec83da3514810059fdecd4167", + "sha256:5bef9bf8deef32814d9565c9df48331e6357eb0b90dabc3ec4f53c44fb34fc73" + ], + "index": "pypi", + "version": "==1.22.0" + }, + "streamlit-option-menu": { + "hashes": [ + "sha256:0b7eae3ffdb0276c81d15750465c72957d57d2f766cb027c586d053519731178", + "sha256:69d1aef6f30f83f29eda3dc9667733bc2e28cd640eb17b4b6ca315f633484c52" + ], + "index": "pypi", + "version": "==0.3.2" + }, + "tenacity": { + "hashes": [ + "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0", + "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0" + ], + "markers": "python_version >= '3.6'", + "version": "==8.2.2" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "toolz": { + "hashes": [ + "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f", + "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194" + ], + "markers": "python_version >= '3.5'", + "version": "==0.12.0" + }, + "tornado": { + "hashes": [ + "sha256:1285f0691143f7ab97150831455d4db17a267b59649f7bd9700282cba3d5e771", + "sha256:3455133b9ff262fd0a75630af0a8ee13564f25fb4fd3d9ce239b8a7d3d027bf8", + "sha256:5e2f49ad371595957c50e42dd7e5c14d64a6843a3cf27352b69c706d1b5918af", + "sha256:81c17e0cc396908a5e25dc8e9c5e4936e6dfd544c9290be48bd054c79bcad51e", + "sha256:90f569a35a8ec19bde53aa596952071f445da678ec8596af763b9b9ce07605e6", + "sha256:9661aa8bc0e9d83d757cd95b6f6d1ece8ca9fd1ccdd34db2de381e25bf818233", + "sha256:a27a1cfa9997923f80bdd962b3aab048ac486ad8cfb2f237964f8ab7f7eb824b", + "sha256:b4e7b956f9b5e6f9feb643ea04f07e7c6b49301e03e0023eedb01fa8cf52f579", + "sha256:d7117f3c7ba5d05813b17a1f04efc8e108a1b811ccfddd9134cc68553c414864", + "sha256:db181eb3df8738613ff0a26f49e1b394aade05034b01200a63e9662f347d4415", + "sha256:ffdce65a281fd708da5a9def3bfb8f364766847fa7ed806821a69094c9629e8a" + ], + "markers": "python_version >= '3.8'", + "version": "==6.3.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + ], + "markers": "python_version < '3.11'", + "version": "==4.5.0" + }, + "tzdata": { + "hashes": [ + "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a", + "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda" + ], + "markers": "python_version >= '2'", + "version": "==2023.3" + }, + "tzlocal": { + "hashes": [ + "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355", + "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2" + ], + "markers": "python_version >= '3.7'", + "version": "==4.3" + }, + "urllib3": { + "hashes": [ + "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", + "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.2" + }, + "validators": { + "hashes": [ + "sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a" + ], + "markers": "python_version >= '3.4'", + "version": "==0.20.0" + }, + "zipp": { + "hashes": [ + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" + ], + "markers": "python_version >= '3.7'", + "version": "==3.15.0" + } + }, + "develop": {} +} diff --git a/08-power-quality-monitor/dashboard/Power_Monitor.py b/08-power-quality-monitor/dashboard/Power_Monitor.py new file mode 100644 index 00000000..dead2a0e --- /dev/null +++ b/08-power-quality-monitor/dashboard/Power_Monitor.py @@ -0,0 +1,110 @@ +import streamlit as st +import pandas as pd +import numpy as np +import snowflake.connector + +hours_in_year = 8760 +elec_emission_factor = 0.85 + +# Initialize connection. +@st.cache_resource +def init_connection(): + return snowflake.connector.connect(**st.secrets["snowflake"]) + +conn = init_connection() + +power_cols = ['Serial Number', 'Device ID', 'Created', 'Apparent Power', 'Frequency', 'Last Current', 'Last Power', 'Power Factor', 'Reactive Power'] +device_cols = ['Device ID', 'Serial Number', 'Maximum Current', 'Maximum Power', 'Maximum Voltage', 'Maximum Power Factor', 'Maximum Reactive Power'] + +# Perform query of Snowflake database. +@st.cache_data(ttl=600) +def run_query(query): + print(f'Running query: {query}') + + with conn.cursor() as cur: + cur.execute(query) + return cur.fetchall() + +# queries to run every time we load the page +devices_query = run_query("select * from devices_vw;") + +# Get data from POWER_VW based on options. +def get_data(): + if len(sort) > 0: + # escape single quotes + sort_clean = sort.replace("'", "''") + query = f'select Serial_Number AS "Serial Number", Device as "Device ID", Created, APPARENT_POWER as "Apparent Power", Frequency, LAST_CURRENT as "Last Current", LAST_POWER as "Last Power", POWER_FACTOR as "Power Factor", REACTIVE_POWER as "Reactive Power" from POWER_VW where SERIAL_NUMBER = \'{sort_clean}\' AND CREATED IS NOT NULL order by CREATED desc limit {num_records};' + else: + query = f'select Serial_Number AS "Serial Number", Device as "Device ID", Created, APPARENT_POWER as "Apparent Power", Frequency, LAST_CURRENT as "Last Current", LAST_POWER as "Last Power", POWER_FACTOR as "Power Factor", REACTIVE_POWER as "Reactive Power" from POWER_VW WHERE CREATED IS NOT NULL order by CREATED desc limit {num_records};' + return run_query(query) + +with st.sidebar: + # Get list of devices and serial numbers for the Sort by Device option. + device_rows = devices_query + device_rows = sorted(device_rows, key=lambda x: x[0]) + + # Get keys from device_rows. + device_names = [row[1] for row in device_rows] + device_keys = [row[0] for row in device_rows] + + """ + ### Options + """ + num_records = st.slider('Records to fetch?', 10, 1000, 100) + sort = st.selectbox('Device',options=device_names, key=device_keys[0]) + show_map = st.checkbox('Show map?', False) + show_charts = st.checkbox('Show charts?', True) + show_table_data = st.checkbox('Show table data?', False) + + +""" +# Blues Power Monitoring Demo +""" +devices_df = pd.DataFrame(device_rows, columns=device_cols) + +st.dataframe(devices_df, hide_index=True, use_container_width=True) + +if show_map: + """ + ### Power Monitor Device Locations + """ + device_locations_query = run_query("select * from device_locations_vw;") + locations_cols = ['Device ID', 'Serial Number', 'latitude', 'longitude'] + + power_locations = pd.DataFrame(device_locations_query, columns=locations_cols)[["latitude", "longitude"]] + + st.map(power_locations) + +if show_table_data or show_charts: + # find the index of the selected device + device_index = device_names.index(sort) + st.write(f'### Data for {sort} ({device_keys[device_index]})') + + data = get_data() + power_df = pd.DataFrame(data, columns=power_cols) + + kwh_per_year = round((power_df['Last Power'][0] / 1000) * hours_in_year, 2) + last_kwh_per_year = round((power_df['Last Power'][1] / 1000) * hours_in_year, 2) + + co2_per_year = round(kwh_per_year * elec_emission_factor, 2) + last_co2_per_year = round(last_kwh_per_year * elec_emission_factor, 2) + + col1, col2 = st.columns(2) + + col1.metric(label="Projected Annual Emissions 🌲", value=f"{co2_per_year} Kg of CO2", delta=f"{round(co2_per_year-last_co2_per_year, 2)} Kg of CO2", delta_color="inverse") + col2.metric(label="Projected Annual Power Use 🔌", value=f"{kwh_per_year} KwH", delta=f"{(kwh_per_year-last_kwh_per_year)} KwH", delta_color="inverse") + + st.divider() + + col1, col2, col3 = st.columns(3) + + col1.metric(label="Apparent Power", value=power_df['Apparent Power'][0], delta=(power_df['Apparent Power'][0]-power_df['Apparent Power'][1]), delta_color="inverse") + col2.metric(label="Last Power", value=f"{power_df['Last Power'][0]} Watts", delta=f"{(power_df['Last Power'][0]-power_df['Last Power'][1])} Watts", delta_color="inverse") + col3.metric(label="Reactive Power", value=f"{power_df['Reactive Power'][0]} VAR", delta=f"{round(power_df['Reactive Power'][0]-power_df['Reactive Power'][1] ,2)} VAR", delta_color="inverse") + + if show_table_data: + st.dataframe(power_df, hide_index=True) + + if show_charts: + st.area_chart(data=power_df[['Apparent Power', 'Last Power','Created']], x='Created', y=['Apparent Power', 'Last Power']) + st.area_chart(data=power_df[['Reactive Power','Created']], x='Created', y=['Reactive Power']) \ No newline at end of file diff --git a/08-power-quality-monitor/dashboard/pages/About.py b/08-power-quality-monitor/dashboard/pages/About.py new file mode 100644 index 00000000..e1e188a0 --- /dev/null +++ b/08-power-quality-monitor/dashboard/pages/About.py @@ -0,0 +1,18 @@ +import streamlit as st + +st.markdown( +""" +# Blues Power Monitoring Demo! + +This demo pulls data from Snowflake that was routed from [this Notehub project](https://notehub.io/project/app:eb43a9ae-0b78-4508-93c2-d39dc511fb70). + +The application in question is a Notecard and Notecarrier-F-based device. The Swan-powered host +application takes readings from a connected Dr. Wattson device that monitors +power through a connected supply and sends those readings to the Notecard. + +Raw JSON is routed to Snowflake using the Snowflake SQL API and transformed into +a structured data tables using views, with a view for `power.qo`, `_session.qo` +events. + +""" +) \ No newline at end of file diff --git a/08-power-quality-monitor/dashboard/requirements.txt b/08-power-quality-monitor/dashboard/requirements.txt new file mode 100644 index 00000000..fdf858eb --- /dev/null +++ b/08-power-quality-monitor/dashboard/requirements.txt @@ -0,0 +1,5 @@ +altair +pandas +streamlit==1.23.0 +snowflake-connector-python==2.7.6 +matplotlib \ No newline at end of file diff --git a/08-power-quality-monitor/firmware/notepower/metadata.h b/08-power-quality-monitor/firmware/notepower/metadata.h index 21ab187d..d9fc1310 100644 --- a/08-power-quality-monitor/firmware/notepower/metadata.h +++ b/08-power-quality-monitor/firmware/notepower/metadata.h @@ -3,10 +3,10 @@ #define PRODUCT_ORG_NAME "" #define PRODUCT_DISPLAY_NAME "Power Quality Monitior" -#define PRODUCT_FIRMWARE_ID "nf8-0.3.0.0" +#define PRODUCT_FIRMWARE_ID "nf8-0.5.0.0" #define PRODUCT_DESC "" #define PRODUCT_MAJOR 0 -#define PRODUCT_MINOR 3 +#define PRODUCT_MINOR 5 #define PRODUCT_PATCH 0 #define PRODUCT_BUILD 0 #define PRODUCT_BUILT __DATE__ " " __TIME__ diff --git a/08-power-quality-monitor/firmware/notepower/notepower.ino b/08-power-quality-monitor/firmware/notepower/notepower.ino index 38f6e803..d56bc12c 100644 --- a/08-power-quality-monitor/firmware/notepower/notepower.ino +++ b/08-power-quality-monitor/firmware/notepower/notepower.ino @@ -77,9 +77,8 @@ void setup() state.envPowerUnder = 0; state.envPowerOver = 0; state.envPowerChange = 0; - - // Load the environment vars for the first time - fetchEnvironmentVariables(state); + state.envIsDemoMode = false; + state.toggleMode = false; lastUpdateMs = millis(); } @@ -94,6 +93,22 @@ void loop() fetchEnvironmentVariables(state); } + if (state.toggleMode) { + J *req = notecard.newRequest("hub.set"); + if (state.envIsDemoMode) { + JAddStringToObject(req, "mode", "continuous"); + JAddNumberToObject(req, "outbound", 1); + } else { + JAddStringToObject(req, "mode", "periodic"); + JAddNumberToObject(req, "outbound", state.envHeartbeatMins > 0 ? state.envHeartbeatMins : 10); + } + if (!notecard.sendRequest(req)) { + Serial.printf("notecard not responding\n"); + } + + state.toggleMode = false; + } + // Read from MCP int readMCPretval = wattson.read(&data, NULL); if (readMCPretval == UpbeatLabs_MCP39F521::SUCCESS) @@ -296,7 +311,6 @@ bool notecardSetup(void) // Done return true; - } void fetchEnvironmentVariables(applicationState &vars) @@ -305,6 +319,7 @@ void fetchEnvironmentVariables(applicationState &vars) J *names = JAddArrayToObject(req, "names"); JAddItemToArray(names, JCreateString("heartbeat_mins")); + JAddItemToArray(names, JCreateString("demo_mode")); JAddItemToArray(names, JCreateString("alert_under_voltage")); JAddItemToArray(names, JCreateString("alert_over_voltage")); JAddItemToArray(names, JCreateString("alert_change_voltage_percent")); @@ -332,6 +347,13 @@ void fetchEnvironmentVariables(applicationState &vars) // Update heartbeat period vars.envHeartbeatMins = JAtoN(JGetString(body, "heartbeat_mins"), NULL); + char *demoMode = JGetString(body, "demo_mode"); + bool lastDemoMode = vars.envIsDemoMode; + vars.envIsDemoMode = (strcmp(demoMode, "true") == 0 || strcmp(demoMode, "1") == 0); + if (vars.envIsDemoMode != lastDemoMode) { + vars.toggleMode = true; + } + // Update the voltage monitoring-related env vars vars.envVoltageUnder = JAtoN(JGetString(body, "alert_under_voltage"), NULL); vars.envVoltageOver = JAtoN(JGetString(body, "alert_over_voltage"), NULL); diff --git a/08-power-quality-monitor/firmware/notepower/power_monitor.h b/08-power-quality-monitor/firmware/notepower/power_monitor.h index 206d90d2..0a06e483 100644 --- a/08-power-quality-monitor/firmware/notepower/power_monitor.h +++ b/08-power-quality-monitor/firmware/notepower/power_monitor.h @@ -22,6 +22,8 @@ typedef struct { typedef struct { uint32_t envHeartbeatMins; + bool envIsDemoMode; + bool toggleMode; float envVoltageUnder; float envVoltageOver; float envVoltageChange;