Skip to content

Conversation

@flora-hofmann-frequenz
Copy link
Collaborator

We have a client request to read out energy metrics. Since the inverter / meter readings are rather unreliable, we want to add a helper function to calculate the energy metric.

@flora-hofmann-frequenz flora-hofmann-frequenz self-assigned this Sep 3, 2024
@github-actions github-actions bot added part:docs Affects the documentation part:tooling Affects the development tooling (CI, deployment, dependency management, etc.) labels Sep 3, 2024
@flora-hofmann-frequenz
Copy link
Collaborator Author

@llucax: This is a very first idea on how to include a helper function to the base client in reporting. Would you please add your input.

pyproject.toml Outdated
"grpcio-tools >= 1.54.2, < 2",
"protobuf >= 4.25.3, < 6",
"frequenz-client-base[grpcio] >= 0.5.0, < 0.6.0",
"pandas == 2.2.2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to make an API client depend on pandas? For me this sounds like it should be put in a higher level.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not at all. It was just a first shot as I am unsure of the data structure in which we want to supply values. I am happy to move away from pandas. The best option may also be connected to data structure that we will use when exposing the states @cwasicki?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we shouldn't use pandas if not urgently required.

Copy link
Contributor

@llucax llucax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so in terms of code structure, I would make the helper function a free function instead of a class method. And probably would take an Iterator or Sequence instead of a list if that's an option.

@noah-kreutzer-frequenz
Copy link

@flora-hofmann-frequenz just for my understanding, what happens if there is a gap in data for the power?

@noah-kreutzer-frequenz
Copy link

I also think that the customer would rather expect the total amount of energy that was consumed or delivered as output, rather than the energy for every timestamp. Something like a "Zählerstand" or at least the difference between the "Zählerstände"

@flora-hofmann-frequenz
Copy link
Collaborator Author

I also think that the customer would rather expect the total amount of energy that was consumed or delivered as output, rather than the energy for every timestamp. Something like a "Zählerstand" or at least the difference between the "Zählerstände"

@noah-kreutzer-frequenz can you please make an example for this? Isn't the total amount dependent on a certain timeframe?

@noah-kreutzer-frequenz
Copy link

I also think that the customer would rather expect the total amount of energy that was consumed or delivered as output, rather than the energy for every timestamp. Something like a "Zählerstand" or at least the difference between the "Zählerstände"

@noah-kreutzer-frequenz can you please make an example for this? Isn't the total amount dependent on a certain timeframe?

So rather than a Timeseries the looking like this

time energy power related to the time frame
0:00 0.0 0
1:00 1.0 1
2:00 2.0 2
3:00 0.0 2
4:00 4.0 4
5:00 0.0 0

I think something like this is what the customer would expect

time total energy power related to the time frame
0:00 0.0 0
1:00 1.0 1
2:00 3.0 2
3:00 3.0 0
4:00 7.0 4
5:00 7.0 0

or even only the the last value

@noah-kreutzer-frequenz
Copy link

and potentially the possoiblity of seperating between consumed energy and delivered energy, so they don't cancel out :))

pyproject.toml Outdated
"grpcio-tools >= 1.54.2, < 2",
"protobuf >= 4.25.3, < 6",
"frequenz-client-base[grpcio] >= 0.5.0, < 0.6.0",
"pandas == 2.2.2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we shouldn't use pandas if not urgently required.

# Convert the timedelta to hours
hours = time_interval.total_seconds() / 3600.0

energy_wh_series = ac_active_power_series * hours
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is what is wanted. Usually the energy metric is integrated over time, so the power would need to be converted to energy and then cumulatively added.

metric_samples: list[MetricSample], time_interval: timedelta
) -> pd.Series:
"""
Calculate the energy metric with AC_ACTIVE_POWER values and time interval.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember whether this should have been from power or the actual energy metrics. For latter, the only thing that needs to be done is to remove the resets. That could be achieved by taking the diff, removing the negatives (reset) and cumulatively sum them up again.


@staticmethod
def calculate_energy_metric(
metric_samples: list[MetricSample], time_interval: timedelta
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a wrapper around list_microgrid_components_data with the same arguments but the metric, which is given.

print(f"RPC failed: {e}")
return

@staticmethod
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good that this is staticmethod, I see this as part of a tooling on top of the client and not the client itself. For now that would be overkill though.

return

@staticmethod
def calculate_energy_metric(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a clearer name, maybe cumulative_energy, total_energy, integrated_power` or so, depending on which approach we go for.

Signed-off-by: flora-hofmann-frequenz <[email protected]>
@flora-hofmann-frequenz
Copy link
Collaborator Author

@cwasicki & @noah-kreutzer-frequenz please take a look at the energy metric logic again. I was trying to implement the following:

consumption = sum(
    e2 - e1 for e1, e2 in zip(filtered_samples, filtered_samples[1:])
    if e2 - e1 > 0
)
production = sum(
    e2 - e1 for e1, e2 in zip(filtered_samples, filtered_samples[1:])
    if e2 - e1 < 0
)

Alongside the previous comments and the option to use power or energy metrics (in case energy is not available).

values = [
sample.value
for sample in metric_samples
if start_time <= sample.timestamp <= end_time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't happen anyway, but typically exclusive timestamps for the end time are used, i.e. < end_time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we actually need to extract the values again and not just loop over the metric_samples (see example for power).

if use_active_power:
# Convert power to energy if using AC_ACTIVE_POWER
time_interval_hours = (
resolution or (end_time - start_time).total_seconds()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second part looks off to me since each power needs to be integrated over it's time period to energy, so would only work for given resolution. You could just use:

consumption = sum(
    m1.value * (m2.timestamp-m1.timestamp).total_seconds() 
    for m in zip(metric_samples, metric_samples[1:] 
    if m1.value > 0) / 3600.0



# pylint: disable=too-many-arguments
async def cumulative_energy(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be part of the client for now, otherwise it could be a separate module.


# pylint: disable=too-many-arguments
async def cumulative_energy(
client: ReportingApiClient,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not part of the client, it needs an URL and key. I guess part of the client makes things simpler now.

@llucax Any thoughts on this? This helper function IMO goes beyond a thin client and could be seen as additional tooling on top of the client. We do think it is overkill to start that now though and wait for further cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not part of the client, it needs an URL and key. I guess part of the client makes things simpler now.

No, it just needs a client, which you should have already instantiated. It really exactly the same, is just a syntax issue. The method would be async def cumulative_energy(self: ReportingApiClient,...) the same, it is just called self by convention. You can actually also call a method like this ReportingApiClient.cumulative_energy(client, ...).

I like utilities being separated because it keeps the client interface clear, otherwise is really hard to draw a line between what should be part of the client what shouldn't.

@llucax Any thoughts on this? This helper function IMO goes beyond a thin client and could be seen as additional tooling on top of the client. We do think it is overkill to start that now though and wait for further cases.

I think we should adopt the https://github.com/frequenz-floss/frequenz-dispatch-python approach if there is a room for a higher level interface. At least IMHO that worked out well. In dispatch we basically have the https://github.com/frequenz-floss/frequenz-client-dispatch-python as a thin wrapper over the API and then frequenz-dispatch-python provides a higher level interface, which makes a lot of assumptions about how the API should be used, so it is much less flexible, but much easier to use for 99% of the cases (scientifically proven :P).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think it will be best to set up frequenz-reporting-python and move the energy metric there :-)

@flora-hofmann-frequenz
Copy link
Collaborator Author

This PR will be closed as it moved to this PR in a new repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

part:docs Affects the documentation part:tooling Affects the development tooling (CI, deployment, dependency management, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants