diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 6e6f35110..101b57078 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -11,15 +11,16 @@ New features * Introduce new reporter to compute profit/loss due to electricity flows: `ProfitOrLossReporter` [see `PR #808 `_ and `PR #844 `_] * Charts visible in the UI can be exported to PNG or SVG formats in a more automated fashion, using the new CLI command flexmeasures show chart [see `PR #833 `_] -* API users can ask for a schedule to take into account an explicit ``power-capacity`` (flex-model) and/or ``site-power-capacity`` (flex-context), thereby overriding any existing defaults for their asset [see `PR #850 `_] +* Chart data visible in the UI can be exported to CSV format [see `PR #849 `_] * Sensor charts showing instantaneous observations can be interpolated by setting the ``interpolate`` sensor attribute to one of the `supported Vega-Lite interpolation methods `_ [see `PR #851 `_] * Scheduling data better distinguishes (e.g. in chart tooltips) when a schedule was the result of a fallback mechanism, by splitting of the fallback mechanism from the main scheduler (as a separate job) [see `PR #846 `_] +* API users can ask for a schedule to take into account an explicit ``power-capacity`` (flex-model) and/or ``site-power-capacity`` (flex-context), thereby overriding any existing defaults for their asset [see `PR #850 `_] Infrastructure / Support ---------------------- * Allow additional datetime conversions to quantitative time units, specifically, from timezone-naive and/or dayfirst datetimes, which can be useful when importing data [see `PR #831 `_] -* Add a new tutorial to explain the use of the `AggregatorReporter` to compute the headroom and the `ProfitOrLossReporter` to compute the cost of running a process [see `PR #825 `_] +* Add a new tutorial to explain the use of the `AggregatorReporter` to compute the headroom and the `ProfitOrLossReporter` to compute the cost of running a process [see `PR #825 `_ and `PR #856 `_] * Script to update dependencies across supported Python versions [see `PR #843 `_] * Test all supported Python versions in our CI pipeline (GitHub Actions) [see `PR #847 `_] * Have our CI pipeline (GitHub Actions) build the Docker image and make a schedule [see `PR #800 `_] diff --git a/documentation/tut/toy-example-expanded.rst b/documentation/tut/toy-example-expanded.rst index 8469dcc54..f7ec952e3 100644 --- a/documentation/tut/toy-example-expanded.rst +++ b/documentation/tut/toy-example-expanded.rst @@ -103,6 +103,20 @@ First, during the sunny hours of the day, when solar power is being send to the Second, charging of the battery is also changed a bit (around 10am), as less can be discharged later. +Moreover, we can use reporters to compute the capacity headroom (see :ref:`tut_toy_schedule_process` for more details). The image below shows that the scheduler is respecting the capacity limits. + +.. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-headroom-pv.png + :align: center +| + +In the case of the scheduler that we ran in the previous tutorial, which did not yet consider the PV, the discharge power would have exceeded the headroom: + +.. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-headroom-nopv.png + :align: center +| + +.. note:: You can add arbitrary sensors to a chart using the attribute ``sensors_to_show``. See :ref:`view_asset-data` for more. + We hope this part of the tutorial shows how to incorporate a limited grid connection rather easily with FlexMeasures. There are more ways to model such settings, but this is a straightforward one. This tutorial showed a quick way to add an inflexible load (like solar power) and a grid connection. In :ref:`tut_toy_schedule_process`, we'll turn to something different: the optimal timing of processes with fixed energy work and duration. diff --git a/documentation/tut/toy-example-reporter.rst b/documentation/tut/toy-example-reporter.rst index 0c069c025..2e6f704b4 100644 --- a/documentation/tut/toy-example-reporter.rst +++ b/documentation/tut/toy-example-reporter.rst @@ -148,8 +148,9 @@ we are going to create share the same `config`. The `config` defines the price s that it will return costs as positive values and revenue as negative values. Still, we need to define the parameters. The three reports share the same structure for the parameters with the following fields: -* `input`: sensor that stores the power/energy flow. The number of sensors is limited to 1. -* `output`: sensor to store the report. We can provide sensors with different resolutions to store the same results at different time scales. + + - `input`: sensor that stores the power/energy flow. The number of sensors is limited to 1. + - `output`: sensor to store the report. We can provide sensors with different resolutions to store the same results at different time scales. .. note:: It's possible to define the `config` and `parameters` in JSON or YAML formats. @@ -242,5 +243,6 @@ Check the results `here `_. The image should b :align: center | + Now, we can compare the results of the reports to the ones we computed manually in :ref:`this table `). Keep in mind that the report is showing the profit of each 15min period and adding them all shows that it matches with our previous results. diff --git a/flexmeasures/ui/static/css/flexmeasures.css b/flexmeasures/ui/static/css/flexmeasures.css index 0fb828474..c7acb09f6 100644 --- a/flexmeasures/ui/static/css/flexmeasures.css +++ b/flexmeasures/ui/static/css/flexmeasures.css @@ -1825,4 +1825,8 @@ div.heading-group { top: 0px; height: 50px; border-width: 0 0 0 16.6666666667px; +} + +#exportToCSVAction { + cursor: pointer; } \ No newline at end of file diff --git a/flexmeasures/ui/static/js/data-utils.js b/flexmeasures/ui/static/js/data-utils.js index 5677d90f2..a6c0ed39e 100644 --- a/flexmeasures/ui/static/js/data-utils.js +++ b/flexmeasures/ui/static/js/data-utils.js @@ -61,3 +61,91 @@ function getValueByNestedKey(obj, key) { return value; } +// From https://stackoverflow.com/a/49332027/13775459 +function toISOLocal(d) { + var z = n => ('0' + n).slice(-2); + var zz = n => ('00' + n).slice(-3); + var off = d.getTimezoneOffset(); + var sign = off > 0? '-' : '+'; + off = Math.abs(off); + + return d.getFullYear() + '-' + + z(d.getMonth()+1) + '-' + + z(d.getDate()) + 'T' + + z(d.getHours()) + ':' + + z(d.getMinutes()) + ':' + + z(d.getSeconds()) + '.' + + zz(d.getMilliseconds()) + + sign + z(off/60|0) + ':' + z(off%60); +} + +// Create a function to convert data to CSV +export function convertToCSV(data) { + if (data.length === 0) { + return ""; + } + + // Extract column names from the first object in the data array + const columns = Object.keys(data[0]); + + // Create the header row + const headerRow = columns.join(',') + '\n'; + + // Create the data rows + const dataRows = data.map(row => { + const rowData = columns.map(col => { + const value = row[col]; + if (typeof value === 'object' && value !== null) { + return value.description || ''; + } else if (col === 'event_start' || col === 'belief_time') { + // Check if the column is a timestamp column + const timestamp = parseInt(value); + if (!isNaN(timestamp)) { + const date = new Date(timestamp); // Convert to Date + // Format the date in ISO8601 format and localize to the specified timezone + // return date.toISOString(); // Use this instead of toISOLocal to get UTC instead + return toISOLocal(date); + } + } else if (col === 'belief_horizon') { + // Check if the column is 'belief_horizon' (duration in ms) + const durationMs = parseInt(value); + if (!isNaN(durationMs)) { + // Check if the duration is zero + if (durationMs === 0) { + return 'PT0H'; + } + + // Check if the duration is negative + const isNegative = durationMs < 0; + + // Calculate absolute duration in seconds + const absDurationSeconds = Math.abs(durationMs) / 1000; + + // Calculate hours, minutes, and seconds + const hours = Math.floor(absDurationSeconds / 3600); + const minutes = Math.floor((absDurationSeconds % 3600) / 60); + const seconds = Math.floor(absDurationSeconds % 60); + + // Format the duration as ISO8601 duration + let iso8601Duration = isNegative ? '-PT' : 'PT'; + if (hours > 0) { + iso8601Duration += hours + 'H'; + } + if (minutes > 0) { + iso8601Duration += minutes + 'M'; + } + if (seconds > 0) { + iso8601Duration += seconds + 'S'; + } + + return iso8601Duration; + } + } + return value; + }); + return rowData.join(','); + }); + + // Combine the header row and data rows + return "data:text/csv;charset=utf-8," + headerRow + dataRows.join('\n'); +} \ No newline at end of file diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 51474f227..3de5bd202 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -200,7 +200,7 @@