Skip to content

Commit

Permalink
Merge pull request #21 from muppet3000/dev
Browse files Browse the repository at this point in the history
Merging Dev into Main ahead of next release
  • Loading branch information
muppet3000 authored Feb 9, 2023
2 parents 4aa2aff + 6b87fa1 commit ab082ed
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: Bug report
about: Log a bug against the integration
title: 'BUG - [Insert Bug Title Here]'
title: BUG - [Insert Bug Title Here]
labels: bug, triage
assignees: ''

Expand Down
22 changes: 22 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an idea for this project
title: ENCHANCEMENT - [TITLE GOES HERE]
labels: enhancement, triage
assignees: ''

---

<!-- PLEASE USE THIS TEMPLATE - It helps to keep requests structured! -->

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The integration has been through a period of instability due to various issues w

The changes made in this repository will be rolled up monthly and then submitted as a Pull-Request to the main Home Assistant Core repository for release as an 'official' Integration.

Note - This Integration works not only with Growatt systems but also Jinko Solar systems (as they appear to be exactly the same systems & API).

# Implementation
On initial release this plugin is an identical copy of the original integration that was part of the Home Assistant release. Going forward, this will be the upstream release of the Integration and the first place that bugs are fixed & new features are added.

Expand Down
5 changes: 2 additions & 3 deletions custom_components/growatt_server_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@

SERVER_URLS = [
"https://server-api.growatt.com/",
"https://server.growatt.com/",
"https://server-us.growatt.com/",
"http://server.smten.com/",
"https://pvplusanz.jinkosolar.com/",
]

DEPRECATED_URLS = [
"https://server.growatt.com/",
]
DEPRECATED_URLS: list[str] = []

DEFAULT_URL = SERVER_URLS[0]

Expand Down
2 changes: 1 addition & 1 deletion custom_components/growatt_server_api/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"codeowners": ["@muppet3000"],
"iot_class": "cloud_polling",
"loggers": ["growattServerApi"],
"version": "1.0.1",
"version": "1.0.2",
"issue_tracker": "https://github.com/muppet3000/homeassistant-growatt_server_api/issues"
}
102 changes: 79 additions & 23 deletions custom_components/growatt_server_api/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class GrowattInverter(SensorEntity):

def __init__(
self, probe, name, unique_id, description: GrowattSensorEntityDescription
):
) -> None:
"""Initialize a PVOutput sensor."""
self.probe = probe
self.entity_description = description
Expand Down Expand Up @@ -178,6 +178,35 @@ def update(self) -> None:
self.probe.update()


def get_pac_value_from_chart(api_pac_value, chart_data):
"""Return the correct pac (Output Power) value based on chart data."""
pac_value = api_pac_value
# If there are no datapoints in the graph set the value to zero.
if len(chart_data) == 0:
_LOGGER.debug("Chart data missing, setting pac to 0")
pac_value = 0
else:
# Establish the last data point in the graph, if the last point
# in the graph is before 'now - 10 minutes' then set the value
# to zero.
sorted_chart_data = sorted(chart_data)
last_updated_dt = dt.parse_datetime(str(sorted_chart_data[-1]))
last_updated = datetime.datetime.combine(
last_updated_dt.date(), last_updated_dt.time(), dt.DEFAULT_TIME_ZONE
)
now = dt.now()
time_diff = now - last_updated
if time_diff > datetime.timedelta(minutes=10):
# If last updated time is < now - 10 minutes, then set the value to zero.
_LOGGER.debug(
"Chart data has no new entry for 10 minutes, setting pac to 0"
)
pac_value = 0
else:
_LOGGER.debug("Chart data has recent entries, leaving pac untouched")
return pac_value


class GrowattData:
"""The class for handling data retrieval."""

Expand Down Expand Up @@ -212,6 +241,12 @@ def update(self):
self.data = inverter_info
elif self.growatt_type == "tlx":
tlx_info = self.api.tlx_detail(self.device_id)
tlx_data = self.api.tlx_data(self.device_id)
tlx_chart_data = tlx_data["invPacData"]

tlx_info["data"]["pac"] = get_pac_value_from_chart(
tlx_info["data"]["pac"], tlx_chart_data
)
self.data = tlx_info["data"]
elif self.growatt_type == "storage":
storage_info_detail = self.api.storage_params(self.device_id)[
Expand All @@ -229,7 +264,8 @@ def update(self):
)

mix_detail = self.api.mix_detail(self.device_id, self.plant_id)
# Get the chart data and work out the time of the last entry, use this as the last time data was published to the Growatt Server
# Get the chart data and work out the time of the last entry, use this
# as the last time data was published to the Growatt Server
mix_chart_entries = mix_detail["chartData"]
sorted_keys = sorted(mix_chart_entries)

Expand All @@ -240,22 +276,40 @@ def update(self):
date_now, last_updated_time, dt.DEFAULT_TIME_ZONE
)

# Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined
# imported from grid value that is the combination of charging AND load consumption
dashboard_data = self.api.dashboard_data(self.plant_id)
# Dashboard values have units e.g. "kWh" as part of their returned string, so we remove it
dashboard_values_for_mix = {
# etouser is already used by the results from 'mix_detail' so we rebrand it as 'etouser_combined'
"etouser_combined": float(
dashboard_data["etouser"].replace("kWh", "")
)
}
# We calculate this value dynamically based on the returned chart data.
# There is no value available on the API that provides the combined
# value of: charging + load consumption
# For each time entry convert it's wattage into kWh, this assumes that
# the wattage value is the same for the whole X minute window (it's the
# only assumption we can make)
# We Multiply the wattage by <TIME PERIOD>/<HOUR>
# (the number of minutes of the time window divided by the number of minutes in an hour)
# to give us the equivalent kWh reading for that X minute window
pac_to_user_today = 0.0
hour_secs = datetime.timedelta(hours=1).total_seconds()
previous_time_val = datetime.time(0, 0, 0) # Start at midnight
for key in sorted_keys:
time_val = datetime.datetime.strptime(key, "%H:%M").time()
# Calculate the difference between this and the previous timestamp
# to determine how long this rate has been used for
timediff_secs = (
datetime.datetime.combine(datetime.date.min, time_val)
- datetime.datetime.combine(
datetime.date.min, previous_time_val
)
).total_seconds()
multiplier = timediff_secs / hour_secs
data_points = mix_chart_entries[key]
pac_to_user_today += float(data_points["pacToUser"]) * multiplier
previous_time_val = time_val

mix_detail["etouser_combined"] = round(pac_to_user_today, 2)

self.data = {
**mix_info,
**mix_totals,
**mix_system_status,
**mix_detail,
**dashboard_values_for_mix,
}
_LOGGER.debug(
"Finished updating data for %s (%s)",
Expand Down Expand Up @@ -298,10 +352,11 @@ def get_data(self, entity_description):
)
diff = float(api_value) - float(previous_value)

# Check if the value has dropped (negative value i.e. < 0) and it has only dropped by a
# small amount, if so, use the previous value.
# Note - The energy dashboard takes care of drops within 10% of the current value,
# however if the value is low e.g. 0.2 and drops by 0.1 it classes as a reset.
# Check if the value has dropped (negative value i.e. < 0) and it has only
# dropped by a small amount, if so, use the previous value.
# Note - The energy dashboard takes care of drops within 10%
# of the current value, however if the value is low e.g. 0.2
# and drops by 0.1 it classes as a reset.
if -(entity_description.previous_value_drop_threshold) <= diff < 0:
_LOGGER.debug(
(
Expand All @@ -318,18 +373,19 @@ def get_data(self, entity_description):
"%s - No drop detected, using API value", entity_description.name
)

# Lifetime total values should always be increasing, they will never reset, however
# the API sometimes returns 0 values when the clock turns to 00:00 local time
# in that scenario we should just return the previous value
# Lifetime total values should always be increasing, they will never reset,
# however the API sometimes returns 0 values when the clock turns to 00:00
# local time in that scenario we should just return the previous value
# Scenarios:
# 1 - System has a genuine 0 value when it it first commissioned:
# - will return 0 until a non-zero value is registered
# 2 - System has been running fine but temporarily resets to 0 briefly at midnight:
# 2 - System has been running fine but temporarily resets to 0 briefly
# at midnight:
# - will return the previous value
# 3 - HA is restarted during the midnight 'outage' - Not handled:
# - Previous value will not exist meaning 0 will be returned
# - This is an edge case that would be better handled by looking up the previous
# value of the entity from the recorder
# - This is an edge case that would be better handled by looking
# up the previous value of the entity from the recorder
if entity_description.never_resets and api_value == 0 and previous_value:
_LOGGER.debug(
(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/growatt_server_api/sensor_types/mix.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@
api_key="lastdataupdate",
device_class=SensorDeviceClass.TIMESTAMP,
),
# Values from 'dashboard_data' API call
# Values calculated from chart data
GrowattSensorEntityDescription(
key="mix_import_from_grid_today_combined",
name="Import from grid today (load + charging)",
Expand Down
3 changes: 1 addition & 2 deletions custom_components/growatt_server_api/sensor_types/tlx.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
Growatt Sensor definitions for the TLX type.
"""Growatt Sensor definitions for the TLX type.
TLX Type is also shown on the UI as: "MIN/MIC/MOD/NEO"
"""
Expand Down
2 changes: 2 additions & 0 deletions info.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ This is a sensor to collect information from your Growatt inverters using [Growa

This will log into your Growatt account and use the first “Plant”, after which it collects the inverters on this plant and creates sensors for these inverters as well as total sensors.

This integration also works with Jinko Solar systems (as they appear to be exactly the same systems & API), the URL is available in the drop-down when selecting your chosen endpoint.

## Supported Systems
- TLX aka MIN/MIC/MOD/NEO
- Mix aka Hybrid
Expand Down

0 comments on commit ab082ed

Please sign in to comment.