Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sun WEG integration #88272

Merged
merged 43 commits into from
Dec 9, 2023
Merged

Sun WEG integration #88272

merged 43 commits into from
Dec 9, 2023

Conversation

rokam
Copy link
Contributor

@rokam rokam commented Feb 16, 2023

Proposed change

Add integration to https://sunweg.net platform for pulling solar energy production data.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Black (black --fast homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
  • Untested files have been added to .coveragerc.

To help with the load of incoming pull requests:

@rokam rokam marked this pull request as ready for review February 17, 2023 03:45
Copy link
Contributor

@Lash-L Lash-L left a comment

Choose a reason for hiding this comment

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

Just a few things I saw, feel free to take anything I say with a grain of salt!

homeassistant/components/sunweg/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/strings.json Outdated Show resolved Hide resolved
homeassistant/components/sunweg/strings.json Outdated Show resolved Hide resolved
@rokam
Copy link
Contributor Author

rokam commented Feb 18, 2023

@Lash-L can you please review it again?

@rokam rokam requested a review from Lash-L February 18, 2023 05:55
Copy link
Contributor

@Lash-L Lash-L left a comment

Choose a reason for hiding this comment

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

Here are my suggestions. I think after this, you will have to wait for a maintainer to give more feedback as I am reaching my limit on what I am knowledgeable about!

homeassistant/components/sunweg/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
tests/components/sunweg/test_init.py Outdated Show resolved Hide resolved
tests/components/sunweg/test_init.py Outdated Show resolved Hide resolved
tests/components/sunweg/test_config_flow.py Outdated Show resolved Hide resolved
tests/components/sunweg/test_config_flow.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
@rokam rokam requested a review from frenck July 25, 2023 16:23
@rokam rokam marked this pull request as ready for review July 25, 2023 16:23
@rokam
Copy link
Contributor Author

rokam commented Oct 16, 2023

@frenck is it possible to make 2023.11?

Copy link
Contributor

@raman325 raman325 left a comment

Choose a reason for hiding this comment

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

some feedback

homeassistant/components/sunweg/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/__init__.py Show resolved Hide resolved
homeassistant/components/sunweg/strings.json Outdated Show resolved Hide resolved
homeassistant/components/sunweg/__init__.py Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
homeassistant/components/sunweg/sensor.py Outdated Show resolved Hide resolved
@home-assistant home-assistant bot marked this pull request as draft November 22, 2023 18:59
@rokam rokam marked this pull request as ready for review November 22, 2023 21:51
@home-assistant home-assistant bot requested a review from raman325 November 22, 2023 21:51
@emontnemery emontnemery dismissed frenck’s stale review November 23, 2023 09:03

CI is now passing

Copy link
Contributor

@raman325 raman325 left a comment

Choose a reason for hiding this comment

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

Nice work, thank you for making all of the requested changes from before, this looks great! I have a couple of ideas on how the code might be slightly improved, but none are required, so it is your call if you want to implement any of them!

Comment on lines +85 to +86
inverter_list = [i for i in self.data.inverters if i.id == inverter_id]
if len(inverter_list) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

not a requirement, just a suggestion/option - you could combine these using a walrus operator

return None
inverter = inverter_list[0]

if device_type == "inverter":
Copy link
Contributor

Choose a reason for hiding this comment

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

might be better to make these string values enums?

return self.data.__dict__.get(variable)

inverter_list = [i for i in self.data.inverters if i.id == inverter_id]
if len(inverter_list) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if len(inverter_list) == 0:
if not inverter_list:

not a requirement, just an idea

entity_description.name,
)
variable = entity_description.api_variable_key
previous_metric = entity_description.native_unit_of_measurement
Copy link
Contributor

Choose a reason for hiding this comment

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

strange variable name. Is it not previous unit?

Comment on lines +44 to +46
login_response = await self.hass.async_add_executor_job(self.api.authenticate)

if not login_response:
Copy link
Contributor

Choose a reason for hiding this comment

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

another candidate for a walrus operator

user_input = FIXTURE_USER_INPUT.copy()

with patch.object(
APIHelper, "authenticate", return_value=SUNWEG_LOGIN_RESPONSE
Copy link
Contributor

Choose a reason for hiding this comment

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

this one screams "convert it to a fixture" to me because it's used multiple times

), patch.object(
APIHelper,
"listPlants",
return_value=[deepcopy(SUNWEG_PLANT_RESPONSE)],
Copy link
Contributor

Choose a reason for hiding this comment

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

same thing re: fixture for this one. You can also avoid the deepcopying by scoping the fixture down to the test

@emontnemery
Copy link
Contributor

@rokam please fix the minor changes requested by @raman325 in a follow-up PR

@emontnemery emontnemery merged commit f567bf6 into home-assistant:dev Dec 9, 2023
51 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Dec 10, 2023
@rokam rokam deleted the features/sunweg branch December 11, 2023 13:13
@rokam rokam mentioned this pull request Dec 13, 2023
20 tasks
Copy link
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

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

Please address the comments in a new PR. Thanks!

):
"""Retrieve from a Plant the desired variable value."""
if device_type == "total":
return self.data.__dict__.get(variable)
Copy link
Member

Choose a reason for hiding this comment

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

Using __dict__ to access attributes is really bad practice.

Copy link
Contributor

Choose a reason for hiding this comment

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

to be clear, getattr should be used to get an attribute by its name

self.data = user_input
return await self.async_step_plant()

async def async_step_plant(self, user_input=None) -> FlowResult:
Copy link
Member

Choose a reason for hiding this comment

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

The type annotation is incomplete. Please type the whole signature, including parameters, when adding type annotations.

"""Handle adding a "plant" to Home Assistant."""
plant_list = await self.hass.async_add_executor_job(self.api.listPlants)

if len(plant_list) == 0:
Copy link
Member

Choose a reason for hiding this comment

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

if not plant_list:


DOMAIN = "sunweg"

PLATFORMS = [Platform.SENSOR]
Copy link
Member

Choose a reason for hiding this comment

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

Please define constants that are only used within a single module within that module. Only shared constants should go in a const.py module.



def get_device_list(
api: APIHelper, config: MappingProxyType[str, Any]
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need the whole config here instead of just the plant id in the config?

self.previous_values: dict = {}

@Throttle(SCAN_INTERVAL)
def update(self) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Why aren't we using a DataUpdateCoordinator instead? That's our recommended way of polling a single api for multiple entities.

https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities


@dataclass
class SunWEGRequiredKeysMixin:
"""Mixin for required keys."""
Copy link
Member

Choose a reason for hiding this comment

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

Nowadays we can set kw_only=True in the dataclass decorator to avoid the need for a mixin for required keys.

https://developers.home-assistant.io/docs/core/entity#example

api = MagicMock()
api.plant = MagicMock(return_value=plant)
api.complete_inverter = MagicMock()
data = SunWEGData(api, 0)
Copy link
Member

Choose a reason for hiding this comment

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

Don't access integration details in the tests. Set up the integration while patching the library and assert core state and mock calls.

https://developers.home-assistant.io/docs/development_testing#writing-tests-for-integrations


def get_data(
self,
entity_description: SunWEGSensorEntityDescription,
Copy link
Member

Choose a reason for hiding this comment

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

The sensor description is a detail of the sensor platform. Either we should move this function to the sensor platform or the function should be refactored to be unaware of the sensor entity description and just work on a base entity description or some other abstraction.

previous_value = self.previous_values.get(variable)
return_value = api_value
if entity_description.api_variable_metric is not None:
entity_description.native_unit_of_measurement = self.get_api_value(
Copy link
Member

@MartinHjelmare MartinHjelmare Dec 17, 2023

Choose a reason for hiding this comment

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

Entity descriptions should not be modified. They are considered frozen.

Generally if the attribute needs to be calculated dynamically it doesn't belong in the entity description. Then it's better to set it inside the entity or pass it to the entity after calculating the attribute.

@home-assistant home-assistant unlocked this conversation Dec 17, 2023
Comment on lines +128 to +189
# If we have a 'drop threshold' specified, then check it and correct if needed
if (
entity_description.previous_value_drop_threshold is not None
and previous_value is not None
and api_value is not None
and previous_metric == entity_description.native_unit_of_measurement
):
_LOGGER.debug(
(
"%s - Drop threshold specified (%s), checking for drop... API"
" Value: %s, Previous Value: %s"
),
entity_description.name,
entity_description.previous_value_drop_threshold,
api_value,
previous_value,
)
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.
if -(entity_description.previous_value_drop_threshold) <= diff < 0:
_LOGGER.debug(
(
"Diff is negative, but only by a small amount therefore not a"
" nightly reset, using previous value (%s) instead of api value"
" (%s)"
),
previous_value,
api_value,
)
return_value = previous_value
else:
_LOGGER.debug(
"%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
# 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:
# - 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
if entity_description.never_resets and api_value == 0 and previous_value:
_LOGGER.debug(
(
"API value is 0, but this value should never reset, returning"
" previous value (%s) instead"
),
previous_value,
)
return_value = previous_value
Copy link
Contributor

@emontnemery emontnemery Dec 18, 2023

Choose a reason for hiding this comment

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

The workarounds do not seem bullet proof, for example if Home Assistant is restarted when we get a wrong reading the accumulated sum will be permanently wrong.

Considering these complications, I don't think it's correct to use SensorStateClass.TOTAL_INCREASING, the state class should be set to SensorStateClass.TOTAL instead for inverter_energy_today and total_energy_today.

Another option would be to not set any state class at all for the *_today readings, there's not really any reason to accumulate statistics for both a grand total and a daily reading.

More details here: https://developers.home-assistant.io/docs/core/entity/sensor#how-to-choose-state_class-and-last_reset

Please fix this in a follow-up.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants