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

Add Gotify component #53050

Closed

Conversation

benjmarshall
Copy link

@benjmarshall benjmarshall commented Jul 15, 2021

Proposed change

Add a new platform for the notify component. Gotify is an open source, self-hosted push notification server.

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)
  • 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.
  • Untested files have been added to .coveragerc.

The integration reached or maintains the following Integration Quality Scale:

  • No score or internal
  • 🥈 Silver
  • 🥇 Gold
  • 🏆 Platinum

To help with the load of incoming pull requests:

@homeassistant
Copy link
Contributor

Hi @benjmarshall,

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@project-bot project-bot bot added this to Needs review in Dev Jul 15, 2021
@MartinHjelmare MartinHjelmare changed the title Add Gotify component. Add Gotify component Jul 15, 2021
@benjmarshall benjmarshall marked this pull request as ready for review July 15, 2021 10:39
homeassistant/components/gotify/manifest.json Outdated Show resolved Hide resolved
homeassistant/components/gotify/manifest.json Outdated Show resolved Hide resolved
homeassistant/components/gotify/notify.py Outdated Show resolved Hide resolved
homeassistant/components/gotify/__init__.py Outdated Show resolved Hide resolved
@benjmarshall
Copy link
Author

@milanmeu you mentioned previously that I should change the way that the platform setup is done. Could you open a PR or provide some guidance on how I should be doing it? Thanks.

@benjmarshall
Copy link
Author

I've updated this so that it should now correctly load the Notification platform and expose a "notify.your_gotify_entry" service.
I would appreciate a further code review at this point to make sure I've done this in the best way possible given the limitations of the Notify component.

The major outstanding issue with this integration is that multiple entries will not work correctly. The is due to the underlying library only exposing a bunch of functions, and using global variables for host/application ID etc, rather than defining a class which can hold a proper configuration for a specific host/application id. I might be able to hack around this by always re-configuring the host/application id with each message to be sent but this is messy. As a better alternative I have opened a PR on the underlying library which converts it to using a class. That way each specific GotifyNotificationService would be able to properly hold it's own configuration.

…tion to use client token to allow the creationg of a new gotify application if required. Re-work to allow multiple config entires to exist. Generate entry name from gotify application name instead of user supplied name.
@benjmarshall
Copy link
Author

benjmarshall commented Jan 17, 2022

Hi All, I've re-worked this component a little today on the basis of comments from @bdraco.

First up the initial configuration has been re-worked to use a Gotify client token, instead of an app token. This allows access to more API endpoints which allows the flexibility to get application details and create new applications if required.

An application token can still be optionally supplied in the config. If supplied then a config entry will be created for the Gotify application that corresponds to this token. If no application token is supplied then a new Gotify application is created on the Gotify server.

The name of the config entry is now generated based on the name of the Gotify application (either created or determined by the supplied app token). Because the name of the config entry is used to create a notify service this needs to be unique. I have added a check to make sure that a unique config entry name is used. A postscript integer (starting with 2) will be appended if the Gotify application would not create a unique config entry name.

These changes should address all of the comments from @bdraco. Additionally these changes allow for multiple config entries to co-exist gracefully.

Due to the fact I have changed more than strictly the suggested changes by @bdraco it would be beneficial to have a further review by the previous comments @milanmeu @TheZoker.

These changes will also require an update to the documentation which I will push soon.
Edit: Documentation now updated.

@github-actions
Copy link

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.

@github-actions github-actions bot added the stale label Apr 26, 2022
@TheLastProject
Copy link
Contributor

Bump for bot (because AFAICT this is again waiting on the Home Assistant team)

@Joshndroid
Copy link

I'll bump this as well... Even though because its taken so long I had to jump over to matrix as my notifier i would still like to see this implemented.

@github-actions github-actions bot removed the stale label Apr 29, 2022
@jackwilsdon
Copy link
Contributor

If it helps at all, I've been using this as a custom component for a few months now and it has been rock solid.

@Hukuma1
Copy link

Hukuma1 commented May 25, 2022

Any chance of a quick primer on how to do that, @jackwilsdon? Would love to run this. I was hoping it would be on HACS if not here yet.

raw_name = DOMAIN + "_" + name.replace(" ", "_").lower()

if DOMAIN in hass.data:
entry_names = []
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 needs moving outside of this if statement (or the while loop needs moving inside of it), as otherwise this function errors out if the gotify domain doesn't exist.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yea, this is a bug for sure. Since the lower half of the function doesn't do anything really if entry_names is empty, I'd recommend a short circuit exit.

if DOMAIN not in hass.data:
    return name

@jackwilsdon
Copy link
Contributor

@Hukuma1 sure:

  1. Download Ben's branch: https://github.com/benjmarshall/core/archive/refs/heads/add_gotify_integration.zip
  2. Copy homeassistant/components/gotify into your custom_components directory (so you end up with custom_components/gotify with all of the contents of homeassistant/components/gotify)
  3. Add a version key to the component's manifest.json:
    --- a/manifest.json
    +++ b/manifest.json
    @@ -1,6 +1,7 @@
     {
       "domain": "gotify",
       "name": "Gotify",
    +  "version": "1.0.0",
       "documentation": "https://www.home-assistant.io/integrations/gotify",
       "codeowners": ["@benjmarshall"],
       "iot_class": "cloud_polling",
  4. Make this change to config_flow.py (I've also left a review comment about this):
    --- a/config_flow.py
    +++ b/config_flow.py
    @@ -46,8 +46,8 @@
     
         raw_name = DOMAIN + "_" + name.replace(" ", "_").lower()
     
    +    entry_names = []
         if DOMAIN in hass.data:
    -        entry_names = []
             for entry in hass.data[DOMAIN]:
                 entry_names.append(hass.data[DOMAIN][entry].data[CONF_NAME])

If you have any issues, feel free to message me on the Home Assistant Community (either using this direct link or via my profile) as I don't wish to start any unrelated discussions on this PR.

Copy link
Contributor

@ViViDboarder ViViDboarder left a comment

Choose a reason for hiding this comment

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

A few small bugs and then some recommendations.

discovery_info = {CONF_NAME: entry.data[CONF_NAME], "entry_id": entry.entry_id}
await hass.async_create_task(
discovery.async_load_platform(
hass, "notify", DOMAIN, discovery_info, hass.data[DOMAIN]
Copy link
Contributor

Choose a reason for hiding this comment

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

The string, notify, should be imported from homeassistant.const.Platform.

Eg.

from homeassistant.const import Platform

Platform.NOTIFY

homeassistant/components/gotify/__init__.py Show resolved Hide resolved
Comment on lines +26 to +30
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass.services.async_remove("notify", entry.data[CONF_NAME])
hass.data[DOMAIN].pop(entry.entry_id)
return True
Copy link
Contributor

Choose a reason for hiding this comment

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

Now the standard pattern here is

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
        hass.data[DOMAIN].pop(entry.entry_id)

    return unload_ok

Again, what you have probably works, but I'm not sure if updating to the new methods is required for merging.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above the notify platform doesn't support config entries.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yea, makes sense.

raw_name = DOMAIN + "_" + name.replace(" ", "_").lower()

if DOMAIN in hass.data:
entry_names = []
Copy link
Contributor

Choose a reason for hiding this comment

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

Yea, this is a bug for sure. Since the lower half of the function doesn't do anything really if entry_names is empty, I'd recommend a short circuit exit.

if DOMAIN not in hass.data:
    return name

Comment on lines +51 to +52
for entry in hass.data[DOMAIN]:
entry_names.append(hass.data[DOMAIN][entry].data[CONF_NAME])
Copy link
Contributor

Choose a reason for hiding this comment

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

As this is a dictionary and you only seem to care about the values, you can instead iterate over hass.data[DOMAIN].values() directly.

You can also simplify this using list comprehension and even avoid empty list initialization.

entry_names = [entry.data[CONF_NAME] for entry in hass.data[DOMAIN].values()]

Comment on lines +54 to +58
name = raw_name
i = 2
while name in entry_names:
name = raw_name + str(i)
return name
Copy link
Contributor

Choose a reason for hiding this comment

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

This will cause an infinite loop if the same name exists since you never remove the name from the entry_names list.

You're probably better off filtering by name in the list above and then using it's length here.

entry_names = [entry.data[CONF_NAME] for entry in hass.data[DOMAIN].values() if entry.data[CONF_NAME] == name]
name = raw_name + str(len(entry_names))

That said, we're not using the actual name values at all, so it could be simplified further.

def sanitise_name(hass: HomeAssistant, name: str) -> str:
    """Make sure we are not going to create and entry with a duplicate name as this would produce duplicate services."""
    num_existing_names = 1
    for entry in hass.data.get(DOMAIN, {}).values():
        if entry.data[CONF_NAME] == name:
            num_existing_names += 1

    name = DOMAIN + "_" + name.replace(" ", "_").lower()
    if num_existing_names == 1:
        return name

    return f"{name}_{num_existing_names}"

return_name = ""

try:
current_applications = await hass.async_add_executor_job(
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this is only used in one block below. It can go within the if CONF_TOKEN in user_input: block.

if user_input[CONF_TOKEN] == app.get("token"):
return_token = user_input[CONF_TOKEN]
return_name = app.get("name")
if not (CONF_TOKEN in user_input) or not return_token:
Copy link
Contributor

Choose a reason for hiding this comment

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

You could drop not (CONF_TOKEN in user_input) and only check not return_token here as return_token is initialized as a falsy value.

) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "https://1.1.1.1", "token": "test-token", "name": "test"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor nit: 1.1.1.1 is a real public IP address. example.com is reserved for these purposes.


result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "1.1.1.1", "token": "test-token", "name": "test"},
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a semantic thing, but technically this is a host address, though it is not a URI. https://www.sistrix.com/ask-sistrix/technical-seo/site-structure/what-is-the-difference-between-a-url-domain-subdomain-hostname-etc

Dev automation moved this from Needs review to Review in progress Dec 6, 2022
@github-actions
Copy link

github-actions bot commented Mar 7, 2023

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.

@github-actions github-actions bot added the stale label Mar 7, 2023
@Pimmetje
Copy link

Bump

@fdw
Copy link

fdw commented May 13, 2023

As anyone still working on this PR (especially @benjmarshall)?

@benjmarshall
Copy link
Author

benjmarshall commented May 13, 2023 via email

@marcelveldt marcelveldt marked this pull request as draft June 7, 2023 10:41
@marcelveldt
Copy link
Member

I've marked this PR as draft, as changes are requested that need to be processed first before we can do another review.
Please un-draft it once it is ready for review again by clicking the "Ready for review" button.

Thanks! 👍

Learn more about our pull request process.

@github-actions github-actions bot removed the stale label Aug 18, 2023
@frenck
Copy link
Member

frenck commented Oct 6, 2023

Because there hasn't been any activity on this PR for quite some time now, I've decided to close it for being stale.

Feel free to re-open this PR when you are ready to pick up work on it again 👍

../Frenck

@frenck frenck closed this Oct 6, 2023
Dev automation moved this from Review in progress to Cancelled Oct 6, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Oct 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
Dev
  
Cancelled
Development

Successfully merging this pull request may close these issues.

None yet