Marketplace versioning with embedded resource

Indeed,I set it up for 4.3 core openHAB in the pom.
Thank you for your modification ! (sorry for the digression)

I don’t think there’s any digression, first of all this is all about trying to figure out all the “complications” of handling add-ons, and specifically it was also about scripting languages providing transformation services, which is very much relevant to you add-on.

If my work ever becomes part of OH, it would be possible to offer different versions for different Core versions, so that you could compile it with such “narrow requirements” as “only 4.3.x” and offer that version to installations running “4.3.x” while offering other (previous?) versions to previous installations. That way it could potentially be pretty seamless for users. I haven’t looked at the OH upgrade logic, but one would think that it would even be possible that the version of the add-on was upgraded accordingly, automatically.

That said, I’m not sure there are many readers still in this thread, I think I’m mostly “talking to myself”, which makes “digressions” even less of an issue :wink:

1 Like

If somebody is still reading this and have any knowledge of OH serialization/deserialization, I’m starting to get a bit fed up with GSON for deserialization from storage.

While it works “ok” for serialization for the REST API (not without issues there either though), the lack of flexibility especially during deserialization makes me have to redesign my classes over and over again. It feels like whatever I try, I hit some obstacle that GSON can’t handle without writing manual, custom deserializers. And, while writing deserializers isn’t that bad, It’s problematic if the (custom deserialized) class has child objects were you would need the “standard deserializer”. It’s also a hassle that you have to make sure that the deserializer is registered with every GSON instance that handles the class in question.

From working with Jackson in the past, it has much more flexibility as far as I can remember, like simply annotating a “setter” method instead of the field for deserializing a specific field, so that you can make a “custom deserializer” directly in the class, without having to register anything anywhere for it to work.

Jackson is already used by OH for YAML and XML as far as I can tell, but for JSON it seems that only GSON is used…? I’m not sure if the required dependencies for doing JSON are already in place, I would have to try using it to discover if everything is there, but chances are that they are.

My question is: Is there some “rule” that OH must only use GSON for JSON, or can I actually use Jackson instead? I think it would make it easier to design classes that work with Storage (JSON DB).

1 Like

That would be good. I remember spending hours on several occasions making an add-on work on different OH versions with the same JAR. I can’t help you on this, but I hope it’s not too complicated and that you will succeed.

2 Likes

I would recommend to open a core issue about that, where we could then ping the core maintainers (I actually think, only members of the GitHub org can do that, so ping either Rich or me and we can then ping the maintainers).

I worked my way around GSON this time too, and while it would be nice to know this on a general basis, I think there are more pressing issues to deal with.

Where I could use some “ping help” is on this simple bugfix that would make marketplace transformations work also after OH is restarted:

Done

2 Likes

Thanks to @J-N-K, suddenly it was merged and there’s no need for pinging :wink:

1 Like

I’m reporting my “progress” here despite not knowing if anybody read it.

I’ve got the “split button” to more or less work now, and just want to explain my thinking: For add-ons without multiple versions, the button won’t be split and will look just like today. For add-ons with multiple (available, depends on compatibility range and published/stable status) versions, the split button will be shown. As only one such add-on currently exists on the marketplace, all testing is done with my “test add-on”.

I strongly dislike “modern” UI design, so I don’t even want to try to make something that “slick”/“modern”. Instead, I’ve just added examples to show how the versions can be styled based on functionality. I really wish somebody will come up with something better if this comes to anything. The styling can be whatever one wants to anyway, the different versions are “tagged” with different CSS classes.

In my example, the circle indicates the currently selected version. The one with a gray background is the currently installed version (if any), the green sheld indicates the latest stable version and the red triangles indicate versions incompatible with the current OH installation. The incompatible ones will only be shown if “Include (Potentially) Incompatible Add-ons” is enabled in the “Add-on Management” settings. It’s probably unlikely that a “real world” add-on would look like this, but I need to test all options.

bilde

Here is a quick demonstration of how this currently works:

2 Likes

This is pretty nice - if the backend follows.

When you have something ready feel free to follow up on GitHub - things are decided there not on this forum.

3 Likes

great job Nadar :+1:

1 Like

@ysc I know it’s not decided here, but this far it’s been even harder to get any response on GitHub, so I’m posting it here hoping to get some feedback and ideas on what solutions to choose/how to do things.

It’s not complete yet, and code-wise there’s a lof of things still to do (headers, JavaDocs, formatting I do when things are “final”, and there’s still a lot of "TODO"s spread around the code). The backend and frontend goes hand in hand, otherwise it wouldn’t work.

It’s already on GitHub, it’s just not in a PR. The Core branch is here:

…and the UI branch is here:

Also, in case anybody is looking at the branches, ignore the commit structure and messages. I always rebase things when I’m done so that the commits are “logical” and completes one thing at a time, with proper descriptions, formatting, JavaDocs etc in place. So, it won’t look like the mess it is now when I’m done. It’s just that I’ve experienced that I usually redesign/refactor things so many times as I go along that it’s not worth it doing all the “formalia”, untll I know that the code will in fact be a part of the final code.

2 Likes

I’m wondering if anybody can give me some insight into the (OH) upgrade process, or some hints of where I can find it.

I’ve looked at UpgradeTool/Upgrader, but can’t find that they do anything to add-ons. Yet I’m pretty sure that I read somewhere about add-ons “disappearing” during upgrade, and that they are supposed to be reinstalled during the process.

The reason I need to figure this out is that it can’t be like it currently is and still work with versions. This is because every marketplace addon basically is uninstalled and reinstalled every time OH is started. I don’t think that’s intentional, but it’s what happens because of the order in which the OSGi components are started and how the “references” are injected. The first “refresh” of add-ons is initialized as soon as the add-on service in question is created, and at this time the add-on handlers aren’t yet registered. As a consequence, the add-on service doesn’t find a handler that confirms that the add-ons are installed, and they are removed but put into a “missing add-on” list where they are attempted to be reinstalled in a different thread. My guess is that timing-wise, the handlers have usually had the time to register before the “reinstallation” starts, so they are reinstalled and people don’t really notice that this is happening.

But, there’s couple of problems with this.

  1. There’s no guarantee that all the handlers are up and running at that time, which would lead to add-ons just “magically being uninstalled” (I have experienced this in my development environment because of frequent restarts and “odd” timings caused by breakpoints etc.).
  2. The add-on handlers have no knowledge of an add-on version, it’s not relevant to them. This could be changed of course, but it would require quite a few changes. I also don’t think it’s necessary for them to be aware of the version. But, because they don’t know anything about versions, this “uninstall-reinstall” mechanism is problematic, because the version information for the add-on is deleted when the add-on is uninstalled (and thus purged from the JSONDB). When the handler later says “Hey, I actually have this add-on here”, the version information has been lost, and the add-on is attempted reinstalled with the “default version” (the version of the add-on that is installed if no version is specified, which is figured out with some logic that chooses the latest stable if possible, and then falls back to “less desirable” options as necessary). This means that any installed version of an add-on that isn’t the “default version” doesn’t survive an OH restart, which breaks the whole version “feature”.

My worry is that I can’t redesign this so that it doesn’t lose the version information without also understanding what happens during upgrade, and what is supposed to happen. Existing add-ons might not be compatible with the new version, and this needs to be handled in some way. It’s this “logic” that I’m trying to find, to take what is done here into account. Ideally the logic could be modified to check if an add-on has another version compatible with the new OH version available and install that instead of possible.

I don’t know as much about the marketplace but I know a little about the official add-ons.

The add-ons that have been installed are listed in userdata/config/org/openhab/addons.config.

During an upgrade, apt, yum, or the upgrade script clears the cache which removes the now stale jar and kar files for all the official add-ons. Obviously this is done outside of OH itself.

When OH comes up that first time after the upgrade, it sees that the required add-ons are not in the cache and downloads them or pulls them from the add-ons.kar file if that’s available to reinstall them. But now OH is a different version and it installs the add-on that corresponds with this new version and that’s how the add-ons get updated.

For marketplace add-ons I think the process is supposed to be similar. However, those get saved to userdata/marketplace instead of the cache and the file that lists what’s been installed is in the jsondb. But I do know that apt/yum/upgrade script clears this folder out when it clears the cache. The marketplace folder is cleared and any add-on listed in the marketplace jsondb file is supposed to be reinstalled, if it has a version that’s compatible with the newly installed version of OH.

However I think that process is currently broken. At least for me what happens is the jsondb file gets emptied out at some point so that post upgrade no marketplace add-ons are installed at all and they all have to be manually reinstalled. What I didn’t know yet is when that file gets emptied.

The other problem is that the pieces isn’t smart enough to jump to a new marketplace entry. For example, if I installed foo for 4.3 and there’s a separate marketplace posting for 5.0, it won’t know to move to the new posting. What you are doing here should address that.

But the overall process, at a high level well be:

  1. OH is stopped
  2. OH is upgraded
  3. cache and marketplace are cleared
  4. OH comes up and sees it’s supposed to have a bunch of add-ons installed but they are not in cache or marketplace so it goes gets the ones that match the running version of OH.

OH doesn’t really upgrade itself directly. Something outside (e.g apt) deletes the old stuff and OH follows the same process as if the add-on is being newly installed.

Thanks for the info. It certainly helps me along the way of understanding the process better, although I assume that userdata/marketplace is only used for bundles - so that this doesn’t apply to “other add-on types”. It probably varies by “add-on type”, I know that transformations for example use org.openhab.marketplace.transformation.json as “storage” for the add-ons themselves - which is probably equivalent of the userdata/marketplace folder for bundles, except that it probably doesn’t get emptied by the external upgrade scripts.

Yes, org.openhab.marketplace.json is in principle emptied on every restart as far as I can understand, depending somewhat on the timing of various components, but it’s hard to imagine a situation where it wouldn’t happen. But, since the information is present in userdata/marketplace, org.openhab.marketplace.transformation.json etc, they are probably reinstalled promply (I haven’t looked that much of how the handling is outside of the marketplace). Before marketplace add-ons are removed from the JSONDB, they are added to a “missing” list (in memory only) and one installation attempt is made by a separate thread. But if that one installation attempt fails for some reason (no internet access, some error during installation etc.), they are basically gone.

Yes, the link between “marketplace ID” and the add-on itself (bundle or otherwise) is fragile. Only the marketplace JSONDB entry has the information to link the two, and there’s no obvious way to “look up” an add-on to find a matching “marketplace ID”, so automatically switching to a different marketplace add-on would be impossible.

This is the point where I really need to nail down the details, because I suspect that there’s no “separate code” to figure out that they should be reinstalled, but that it’s the very same logic that causes the “disappearing add-ons” and the loss of the version information that makes sure that they are reinstalled. Thus, any changes to this logic will probably impact upgrade behavior too.

I think I need to study how it works for non-marketplace add-ons a bit, maybe that will make me figure out if I have the full picture.

This is actually problematic with my implementation of dependencies too, I hadn’t quite realized that, but what you “depend on” is the add-on ID. That means that you can depend on an official or locally installed bundle, or a marketplace add-on, but it can’t “be agnostic” about the source. So, if you download a marketplace bundle manually and add it as a JAR, that dependency would be unforfilled. I’ll have to see later if I can find a way around this, too much other stuff in my head right now.

edit: Add-ons have both “uid” and “id” fields. Maybe the “id” field can be used to identify add-ons independencly of source (the “uid” field is the one using the forum topic indentifier).

Everything else from the marketplace can only be upgraded manually by removing and then re-adding. There is no automatic upgrade process at all.

That is the installation. Anything in those files just get loaded same as any other config from any of the other jsondb files. The installation happens when it gets added to that jsondb file and until you manually remove it it remains there unmodified.

Note that this isn’t necessarily unwanted behavior. If it got removed or automatically upgraded you couldn’t, for example, install and manually customize a widget that mostly does what you want but needs some tweaks.

Ok, I see that it might not be clearly defined exactly what “installed” means. First the add-ons that exist in org.openhab.marketplace.json is included, after which the handlers are asked to “confirm” that they are installed. If not, they go to the “missing” list. Then, after pulling information about all the add-ons from the marketplace, the add-on handlers are queried if each and every add-on found on the marketplace is “installed”. The handlers doesn’t know about org.openhab.marketplace.json - they only know about the other form of storage, e.g userdata/marketplace and will deem that they are “installed” if they exist there. If the handlers confirm that one of those pulled from the marketplace is “installed”, they will be added to the list of installed add-ons too. Then the list of add-ons is “cleaned” to remove duplicates and only keep the “installed” version in the list. I think this “duality” of how an add-on can be considered installed is part of what makes this “complicated”. Most of this logic can be found in this one method:

Yes, getting the “rules” for how to handle such things right can be complicated. Some would say that “newest is always best”, I would say that automatic upgrade should probably only be attempted if the installed version is deemed incompatible with the current version and a compatible version exists on the marketplace.

About UI widgets, they will refuse to install if another widget exists with the same uid, even if that doesn’t come from the marketplace. But an “upgrade” would replace the modified version, because it would first uninstall the “old” one. I think all that’s neede to “decouple it” from handling as an add-on is to remove the marketplace:xxxxxx tag from the widget though, but I can’t say for sure. It might never be a good idea to automatically upgrade UI widgets, in case they have been modified. It would, I guess, be possible to first compare their content with the “original” to determine if they have been modified, but such things gets complicated quickly. There are so many corner cases.

The solution for transformations might actually be better in that sense, because if has a read-only JSON storage for those installed from the marketplace. That way, they are impossible to modify. Instead, there should be an easy way to copy them if you wish to modify them, but that copy would be completely uncoupled with the marketplace.

I haven’t really thought this through, I’m just thinking out loud, but I think it would be desirable if at least some add-ons where upgraded automatically if the existing version no longer works with the OH version.

I just discovered another “issue”, although I’m not sure exactly how this should ideally be handled. As part of my testing, I installed this addon. I didn’t care that it’s not for the current version, I was going to look at the installation process itself, not use the actual add-on. It did install seemingly without problems, but when I went to Developer Tools → Widgets and tried to open/look at the content of the widget, things started to act “strange”.

First, the browser more or less froze, the page with the “yaml” never loaded. I checked in the browser console, and there were lots of errors (I can’t paste it all here, it’s long and frankly not that relevant):

[Vue warn]: Error in nextTick: "InvalidCharacterError: Failed to execute 'createElement' on 'Document': The tag name provided ('<!doctype html> <html lang="en"
data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" data-a11y-animated-images="system" data-a11y-link-underlines="true"
>


<head> <meta charset="utf-8"> <link rel="dns-prefetch" href="https://github.githubassets.com"> <link rel="dns-prefetch" href="https://avatars.githubusercontent.com"> <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com"> <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/"> <link rel="preconnect" href="https://github.githubassets.com" crossorigin> <link rel="preconnect" href="https://avatars.githubusercontent.com">

…and:

InvalidCharacterError: Failed to execute 'createElement' on 'Document': The tag name provided ('<!doctype html> <html lang="en"
data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" data-a11y-animated-images="system" data-a11y-link-underlines="true"
>

I tried to investigate why this happened, with OH still running in Eclipse, but everything was very sluggish and after a while Eclipse itself threw a stack overflow error and a warning told me I should close Eclipse of bad things would happen. I think the stack overflow came from OH, and that it somehow managed to “tear down Eclipse with it”.

So, the question is: Why all this strange behavior, both from the browser and OH. Once Eclipse was up and running again, I looked in the JSONDB, and found that the “content” of the widget which was:

\u003c!DOCTYPE html\u003e \u003chtml lang\u003d\"en\"\ndata-color-mode\u003d\"auto\" data-light-theme\u003d\"light\" data-dark-theme\u003d\"dark\" data-a11y-animated-images\u003d\"system\" data-a11y-link-underlines\u003d\"true\"\n\u003e\n\n\n\u003chead\u003e \u003cmeta charset\u003d\"utf-8\"\u003e \u003clink rel\u003d\"dns-prefetch\" href\u003d\"https://github.githubassets.com\"\u003e \u003clink rel\u003d\"dns-prefetch\" href\u003d\"https://avatars.githubusercontent.com\"\u003e \u003clink rel\u003d\"dns-prefetch\" href\u003d\"https://github-cloud.s3.amazonaws.com\"\u003e \u003clink rel\u003d\"dns-prefetch\" href\u003d\"https://user-images.githubusercontent.com/\"\u003e \u003clink rel\u003d\"preconnect\" href\u003d\"https://github.githubassets.com\" crossorigin\u003e \u003clink rel\u003d\"preconnect\" href\u003d\"https://avatars.githubusercontent.com\"\u003e\n\n\n\n\u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/light-7aa84bb7e11e.css\" /\u003e\u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/dark-f65db3e8d171.css\" /\u003e\u003clink data-color-theme\u003d\"dark_dimmed\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/dark_dimmed-a8258e3c6dda.css\" /\u003e\u003clink data-color-theme\u003d\"dark_high_contrast\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/dark_high_contrast-7e97d834719c.css\" /\u003e\u003clink data-color-theme\u003d\"dark_colorblind\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/dark_colorblind-01d869f460be.css\" /\u003e\u003clink data-color-theme\u003d\"light_colorblind\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/light_colorblind-534f3e971240.css\" /\u003e\u003clink data-color-theme\u003d\"light_high_contrast\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/light_high_contrast-a8cc7d138001.css\" /\u003e\u003clink data-color-theme\u003d\"light_tritanopia\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/light_tritanopia-35e9dfdc4f9f.css\" /\u003e\u003clink data-color-theme\u003d\"dark_tritanopia\" crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" data-href\u003d\"https://github.githubassets.com/assets/dark_tritanopia-cf4cc5f62dfe.css\" /\u003e\n\u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/primer-primitives-d9abecd14f1e.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/primer-93aded0ee8a1.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/global-1f2860c46060.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/github-a88fdad54119.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/repository-4fce88777fa8.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/code-0210be90f4d3.css\" /\u003e\n\n\n\n\u003cscript type\u003d\"application/json\" id\u003d\"client-env\"\u003e{\"locale\":\"en\",\"featureFlags\":[\"bypass_copilot_indexing_quota\",\"copilot_immersive_file_preview\",\"copilot_new_references_ui\",\"copilot_attach_folder_reference\",\"copilot_chat_repo_custom_instructions_preview\",\"copilot_chat_retry_on_error\",\"copilot_chat_persist_submitted_input\",\"copilot_conversational_ux_history_refs\",\"copilot_chat_shared_topic_indicator\",\"copilot_chat_shared_repo_sso_banner\",\"copilot_editor_upsells\",\"copilot_dotcom_chat_reduce_telemetry\",\"copilot_free_limited_user\",\"copilot_implicit_context\",\"copilot_no_floating_button\",\"copilot_smell_icebreaker_ux\",\"copilot_new_markdown_renderer\",\"experimentation_azure_variant_endpoint\",\"failbot_handle_non_errors\",\"geojson_azure_maps\",\"ghost_pilot_confidence_truncation_25\",\"ghost_pilot_confidence_truncation_40\",\"github_models_o3_mini_streaming\",\"github_models_per_chunk_timeout\",\"hovercard_accessibility\",\"issues_react_remove_placeholders\",\"issues_react_blur_item_picker_on_close\",\"issues_react_include_bots_in_pickers\",\"marketing_pages_search_explore_provider\",\"remove_child_patch\",\"sample_network_conn_type\",\"swp_enterprise_contact_form\",\"site_copilot_acc\",\"site_copilot_vscode_link_update\",\"site_proxima_australia_update\",\"issues_react_create_milestone\",\"issues_react_cache_fix_workaround\",\"lifecycle_label_name_updates\"]}\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/wp-runtime-25c19eafd594.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_oddbird_popover-polyfill_dist_popover_js-9da652f58479.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_arianotify-polyfill_ariaNotify-polyfill_js-node_modules_github_mi-3abb8f-d7e6bc799724.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_failbot_failbot_ts-25697e0f4c47.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/environment-04ca94cb6e8a.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_primer_behaviors_dist_esm_index_mjs-0dbb79f97f8f.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_selector-observer_dist_index_esm_js-f690fd9ae3d5.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_relative-time-element_dist_index_js-f6da4b3fa34c.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_auto-complete-element_dist_index_js-node_modules_github_catalyst_-8e9f78-a74b4e0a8a6b.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_text-expander-element_dist_index_js-78748950cb0c.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_filter-input-element_dist_index_js-node_modules_github_remote-inp-b5f1d7-a1760ffda83d.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_markdown-toolbar-element_dist_index_js-ceef33f593fa.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_file-attachment-element_dist_index_js-node_modules_primer_view-co-c44a69-f0c8a795d1fd.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/github-elements-90f965d59632.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/element-registry-d018d1dc6e26.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_braintree_browser-detection_dist_browser-detection_js-node_modules_githu-bb80ec-72267f4e3ff9.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_lit-html_lit-html_js-be8cb88f481b.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_mini-throttle_dist_index_js-node_modules_morphdom_dist_morphdom-e-7c534c-a4a1922eb55f.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_turbo_dist_turbo_es2017-esm_js-e3cbe28f1638.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_remote-form_dist_index_js-node_modules_delegated-events_dist_inde-893f9f-6cf3320416b8.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_color-convert_index_js-e3180fe3bcb3.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_quote-selection_dist_index_js-node_modules_github_session-resume_-69cfcc-ccab506ecf8c.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_updatable-content_updatable-content_ts-439f48470426.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/app_assets_modules_github_behaviors_task-list_ts-app_assets_modules_github_sso_ts-ui_packages-900dde-03160297135f.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/app_assets_modules_github_sticky-scroll-into-view_ts-5316a27f9573.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/app_assets_modules_github_behaviors_ajax-error_ts-app_assets_modules_github_behaviors_include-87a4ae-0a6bb0ce2586.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/app_assets_modules_github_behaviors_commenting_edit_ts-app_assets_modules_github_behaviors_ht-83c235-42e06545c1fa.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/behaviors-a8a11c816f0b.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_delegated-events_dist_index_js-node_modules_github_catalyst_lib_index_js-f6223d90c7ba.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/notifications-global-197c9e29d935.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_mini-throttle_dist_index_js-node_modules_github_catalyst_lib_inde-dbbea9-26cce2010167.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/code-menu-32e8c76a1001.js\"\u003e\u003c/script\u003e\n\u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/primer-react-8ce8f9e2d741.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/react-core-d18f849a581f.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/react-lib-f09868a8643f.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/octicons-react-611691cca2f6.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_emotion_is-prop-valid_dist_emotion-is-prop-valid_esm_js-node_modules_emo-62da9f-2df2f32ec596.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_mini-throttle_dist_index_js-node_modules_stacktrace-parser_dist_s-e7dcdd-f7cc96ebae76.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_oddbird_popover-polyfill_dist_popover-fn_js-55fea94174bf.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_dompurify_dist_purify_js-b89b98661809.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_primer_live-region-element_dist_esm_index_js-node_modules_tanstack_query-1fdea8-83f2f37789a4.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_lodash-es__Stack_js-node_modules_lodash-es__Uint8Array_js-node_modules_l-4faaa6-4a736fde5c2f.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_lodash-es__baseIsEqual_js-8929eb9718d5.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/vendors-node_modules_github_hydro-analytics-client_dist_analytics-client_js-node_modules_gith-853b24-f2006d2a5b98.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_aria-live_aria-live_ts-ui_packages_promise-with-resolvers-polyfill_promise-with-r-014121-adb7f840e9e5.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_paths_index_ts-4558f7eebed5.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_ref-selector_RefSelector_tsx-81e5bb727b3a.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_code-view-shared_utilities_web-worker_ts-ui_packages_code-view-shared_worker-jobs-dcdfcd-c93e9c09883e.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_code-view-shared_hooks_use-canonical-object_ts-ui_packages_code-view-shared_hooks-a6859a-930930d9b033.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/ui_packages_repos-file-tree-view_repos-file-tree-view_ts-ui_packages_feature-request_FeatureR-648c3b-352c9fdd3275.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/app_assets_modules_github_blob-anchor_ts-ui_packages_code-nav_code-nav_ts-ui_packages_filter--8253c1-6e376eb94aa9.js\"\u003e\u003c/script\u003e \u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/react-code-view-c9f911945db9.js\"\u003e\u003c/script\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/primer-react.faedbc54f89d0442e933.module.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/react-code-view.ab7d8fac328c00e5e0cc.module.css\" /\u003e\n\u003cscript crossorigin\u003d\"anonymous\" defer\u003d\"defer\" type\u003d\"application/javascript\" src\u003d\"https://github.githubassets.com/assets/notifications-subscriptions-menu-7eba7d01e0ba.js\"\u003e\u003c/script\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/primer-react.faedbc54f89d0442e933.module.css\" /\u003e \u003clink crossorigin\u003d\"anonymous\" media\u003d\"all\" rel\u003d\"stylesheet\" href\u003d\"https://github.githubassets.com/assets/notifications-subscriptions-menu.1bcff9205c241e99cff2.module.css\" /\u003e\n\n\u003ctitle\u003eoh3-widgetHeating/oh3-widgetHeating.yaml at main · NRquadrat/oh3-widgetHeating · GitHub\u003c/title\u003e\n\n\n\u003cmeta name\u003d\"route-pattern\" content\u003d\"/:user_id/:repository/blob/*name(/*path)\" data-turbo-transient\u003e \u003cmeta name\u003d\"route-controller\" content\u003d\"blob\" data-turbo-transient\u003e \u003cmeta name\u003d\"route-action\" content\u003d\"show\" data-turbo-transient\u003e\n\n\u003cmeta name\u003d\"current-catalog-service-hash\" content\u003d\"f3abb0cc802f3d7b95fc8762b94bdcb13bf39634c40c357301c4aa1d67a256fb\"\u003e\n\n\u003cmeta name\u003d\"request-id\" content\u003d\"9046:3C735A:51B4BE:54B09E:67A6A0BF\" data-pjax-transient\u003d\"true\"/\u003e\u003cmeta name\u003d\"html-safe-nonce\" content\u003d\"ce6ea9f1ffc3f1184e5f96b9f293c560fd82c3773343f35c4d16062385652f01\" data-pjax-transient\u003d\"true\"/\u003e\u003cmeta name\u003d\"visitor-payload\" content\u003d\"eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5MDQ2OjNDNzM1QTo1MUI0QkU6NTRCMDlFOjY3QTZBMEJGIiwidmlzaXRvcl9pZCI6IjMxNjAwNzk3MTIyNjc5Njg3MDMiLCJyZWdpb25fZWRnZSI6ImZyYSIsInJlZ2lvbl9yZW5kZXIiOiJmcmEifQ\u003d\u003d\" data-pjax-transient\u003d\"true\"/\u003e\u003cmeta name\u003d\"visitor-hmac\" content\u003d\"9ea19db4df108f33256706f28e2d2925370c75908a9aa1ddc06091102ad5e640\" data-pjax-transient\u003d\"true\"/\u003e\n\n\u003cmeta name\u003d\"hovercard-subject-tag\" content\u003d\"repository:343098514\" data-turbo-transient\u003e\n\n\u003cmeta name\u003d\"github-keyboard-shortcuts\" content\u003d\"repository,source-code,file-tree,copilot\" data-turbo-transient\u003d\"true\" /\u003e\n\n\u003cmeta name\u003d\"selected-link\" value\u003d\"repo_source\" data-turbo-transient\u003e \u003clink rel\u003d\"assets\" href\u003d\"https://github.githubassets.com/\"\u003e\n\u003cmeta name\u003d\"google-site-verification\" content\u003d\"Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I\"\u003e\n\u003cmeta name\u003d\"octolytics-url\" content\u003d\"https://collector.github.com/github/collect\" /\u003e\n\u003cmeta name\u003d\"analytics-location\" content\u003d\"/\u0026lt;user-name\u0026gt;/\u0026lt;repo-name\u0026gt;/blob/show\" data-turbo-transient\u003d\"true\" /\u003e\n\n\n\n\n\n\u003cmeta name\u003d\"user-login\" content\u003d\"\"\u003e\n\n\n\u003cmeta name\u003d\"viewport\" content\u003d\"width\u003ddevice-width\"\u003e\n\n\n\u003cmeta name\u003d\"description\" content\u003d\"Contribute to NRquadrat/oh3-widgetHeating development by creating an account on GitHub.\"\u003e\n\u003clink rel\u003d\"search\" type\u003d\"application/opensearchdescription+xml\" href\u003d\"/opensearch.xml\" title\u003d\"GitHub\"\u003e\n\u003clink rel\u003d\"fluid-icon\" href\u003d\"https://github.com/fluidicon.png\" title\u003d\"GitHub\"\u003e \u003cmeta property\u003d\"fb:app_id\" content\u003d\"1401488693436528\"\u003e \u003cmeta name\u003d\"apple-itunes-app\" content\u003d\"app-id\u003d1477376905, app-argument\u003dhttps://github.com/NRquadrat/oh3-widgetHeating/blob/main/oh3-widgetHeating.yaml\" /\u003e\n\u003cmeta name\u003d\"twitter:image\" content\u003d\"https://opengraph.githubassets.com/27a2f66bebed6778a39c788250b1d4785ed4d83d5d439800cca92e069ba24ec2/NRquadrat/oh3-widgetHeating\" /\u003e\u003cmeta name\u003d\"twitter:site\" content\u003d\"@github\" /\u003e\u003cmeta name\u003d\"twitter:card\" content\u003d\"summary_large_image\" /\u003e\u003cmeta name\u003d\"twitter:title\" content\u003d\"oh3-widgetHeating/oh3-widgetHeating.yaml at main · NRquadrat/oh3-widgetHeating\" /\u003e\u003cmeta name\u003d\"twitter:description\" content\u003d\"Contribute to NRquadrat/oh3-widgetHeating development by creating an account on GitHub.\" /\u003e \u003cmeta property\u003d\"og:image\" content\u003d\"https://opengraph.githubassets.com/27a2f66bebed6778a39c788250b1d4785ed4d83d5d439800cca92e069ba24ec2/NRquadrat/oh3-widgetHeating\" /\u003e\u003cmeta property\u003d\"og:image:alt\" content\u003d\"Contribute to NRquadrat/oh3-widgetHeating development by creating an account on GitHub.\" /\u003e\u003cmeta property\u003d\"og:image:width\" content\u003d\"1200\" /\u003e\u003cmeta property\u003d\"og:image:height\" content\u003d\"600\" /\u003e\u003cmeta property\u003d\"og:site_name\" content\u003d\"GitHub\" /\u003e\u003cmeta property\u003d\"og:type\" content\u003d\"object\" /\u003e\u003cmeta property\u003d\"og:title\" content\u003d\"oh3-widgetHeating/oh3-widgetHeating.yaml at main · NRquadrat/oh3-widgetHeating\" /\u003e\u003cmeta property\u003d\"og:url\" content\u003d\"https://github.com/NRquadrat/oh3-widgetHeating/blob/main/oh3-widgetHeating.yaml\" /\u003e\u003cmeta property\u003d\"og:description\" content\u003d\"Contribute to NRquadrat/oh3-widgetHeating development by creating an account on GitHub.\" /\u003e\n\n\n\n\n\u003cmeta name\u003d\"hostname\" content\u003d\"github.com\"\u003e\n\n\n\u003cmeta name\u003d\"expected-hostname\" content\u003d\"github.com\"\u003e\n\n\u003cmeta http-equiv\u003d\"x-pjax-version\" content\u003d\"735b48f7785ec392ba54588acbe5248ead02b53bf489898c8471152230410363\" data-turbo-track\u003d\"reload\"\u003e \u003cmeta http-equiv\u003d\"x-pjax-csp-version\" content\u003d\"ace39c3b6632770952207593607e6e0be0db363435a8b877b1f96abe6430f345\" data-turbo-track\u003d\"reload\"\u003e \u003cmeta http-equiv\u003d\"x-pjax-css-version\" content\u003d\"686ae14b0d1376d2af90b99f741dc2717978409537c0abc07d0f96ff699b7b72\" data-turbo-track\u003d\"reload\"\u003e \u003cmeta http-equiv\u003d\"x-pjax-js-version\" content\u003d\"eea4c31b632e564c10243ea61ef53bd3961bffb4e92ecaafc32852ec2ba86826\" data-turbo-track\u003d\"reload\"\u003e\n\u003cmeta name\u003d\"turbo-cache-control\" content\u003d\"no-preview\" data-turbo-transient\u003d\"\"\u003e\n\u003cmeta name\u003d\"turbo-cache-control\" content\u003d\"no-cache\" data-turbo-transient\u003e\n\u003cmeta data-hydrostats\u003d\"publish\"\u003e \u003cmeta name\u003d\"go-import\" content\u003d\"github.com/NRquadrat/oh3-widgetHeating git https://github.com/NRquadrat/oh3-widgetHeating.git\"\u003e\n\u003cmeta name\u003d\"octolytics-dimension-user_id\" content\u003d\"79790877\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-user_login\" content\u003d\"NRquadrat\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-repository_id\" content\u003d\"343098514\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-repository_nwo\" content\u003d\"NRquadrat/oh3-widgetHeating\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-repository_public\" content\u003d\"true\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-repository_is_fork\" content\u003d\"false\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-repository_network_root_id\" content\u003d\"343098514\" /\u003e\u003cmeta name\u003d\"octolytics-dimension-repository_network_root_nwo\" content\u003d\"NRquadrat/oh3-widgetHeating\" /\u003e\n\n\n\n\n\u003cmeta name\u003d\"turbo-body-classes\" content\u003d\"logged-out env-production page-responsive\"\u003e\n\n\u003cmeta name\u003d\"browser-stats-url\" content\u003d\"https://api.github.com/_private/browser/stats\"\u003e\n\u003cmeta name\u003d\"browser-errors-url\" content\u003d\"https://api.github.com/_private/browser/errors\"\u003e\n\u003clink rel\u003d\"mask-icon\" href\u003d\"https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg\" color\u003d\"#000000\"\u003e \u003clink rel\u003d\"alternate icon\" class\u003d\"js-site-favicon\" type\u003d\"image/png\" href\u003d\"https://github.githubassets.com/favicons/favicon.png\"\u003e \u003clink rel\u003d\"icon\" class\u003d\"js-site-favicon\" type\u003d\"image/svg+xml\" href\u003d\"https://github.githubassets.com/favicons/favicon.svg\" data-base-href\u003d\"https://github.githubassets.com/favicons/favicon\"\u003e\n\u003cmeta name\u003d\"theme-color\" content\u003d\"#1e2327\"\u003e \u003cmeta name\u003d\"color-scheme\" content\u003d\"light dark\" /\u003e\n\n\u003clink rel\u003d\"manifest\" href\u003d\"/manifest.json\" crossOrigin\u003d\"use-credentials\"\u003e\n\u003c/head\u003e\n\u003cbody class\u003d\"logged-out env-production page-responsive\" style\u003d\"word-wrap

This isn’t exactly valid YAML, so it’s no wonder there were problems processing it. But, two questions present themselves to me:

  1. Why did this happen
  2. Is there too little “protection” against invalid content since it’s possible to install and then “crash OH pretty hard” by feeding it invalid content. If a bundle was corrupted like this, Java would refuse to run it, but YAML and JSON content has no such protection.

I think I’ve figured out 1). Looking at the add-on, I found that the “resource link” is: https://github.com/NRquadrat/oh3-widgetHeating/blob/main/oh3-widgetHeating.yaml

That opens fine in a browser, but is not a link to the file itself. The correct link should have been: https://raw.githubusercontent.com/NRquadrat/oh3-widgetHeating/refs/heads/main/oh3-widgetHeating.yaml

This is an “easy mistake” for people to do when linking their resources, and is something that probably should be expected. OH itself only looks at the extension, and as long as the URL ends with .yaml or .json, it’s assumed a valid resource.

One could imagine making some “automatic translation” of github.com resources to raw.githubusercontent.com if such a link is found, but I’m not sure how easy it is to do. After all, the github.com the express what they point to quite differently (blob/main is transformed into “gittish” refs/heads/main). Since GitHub is a frequently used source, it could be worth trying to mitigate the risk of this happening, but I don’t think it’s easy to figure out a relable way to convert such an URL. If somebody has some knowledge of a way to do this, please correct me.

A straight “ban” on github.com resource URLs would be another way, but I’m not sure if there could be situations where this would be undesirable. I don’t know what power is in Discord, maybe it could warn the author?

In any case, the real problem is 2). One thing is to try to handle GitHub specifically, but the link doesn’t have to be to GitHub, and it’s certainly impossible to guard against this on a general basis.

What I don’t quite understand is how this got “installed” in the first place, because I know that I’ve seen errors in the past that e.g “the widget is invalid” when trying to install. I would think that a good start would be to reject anything that can’t be parsed by the YAML or JSON parsers - but maybe this content for some reason “passed that test”. I’d have to investigate further. If a text starts with the “right” characters, it might be that pretty much any content can pass as YAML?

As I thought, the content is parsed by the YAML parser. For some reason it accepts this as YAML, converting the “source” (snippet):

<!DOCTYPE html>
<html
  lang="en"
  
  data-color-mode="auto" data-light-theme="light" data-dark-theme="dark"
  data-a11y-animated-images="system" data-a11y-link-underlines="true"
  
  >



  <head>
    <meta charset="utf-8">
  <link rel="dns-prefetch" href="https://github.githubassets.com">
  <link rel="dns-prefetch" href="https://avatars.githubusercontent.com">
  <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com">
  <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/">
  <link rel="preconnect" href="https://github.githubassets.com" crossorigin>
  <link rel="preconnect" href="https://avatars.githubusercontent.com">

  


  <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light-7aa84bb7e11e.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark-f65db3e8d171.css" /><link data-color-theme="dark_dimmed" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_dimmed-a8258e3c6dda.css" /><link data-color-theme="dark_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_high_contrast-7e97d834719c.css" /><link data-color-theme="dark_colorblind" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_colorblind-01d869f460be.css" /><link data-color-theme="light_colorblind" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_colorblind-534f3e971240.css" /><link data-color-theme="light_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_high_contrast-a8cc7d138001.css" /><link data-color-theme="light_tritanopia" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_tritanopia-35e9dfdc4f9f.css" /><link data-color-theme="dark_tritanopia" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_tritanopia-cf4cc5f62dfe.css" />

    <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-primitives-d9abecd14f1e.css" />
    <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-93aded0ee8a1.css" />
    <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-1f2860c46060.css" />
    <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/github-a88fdad54119.css" />
  <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/repository-4fce88777fa8.css" />
<link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/code-0210be90f4d3.css" />

…to a RootUIComponent with UID 538a6efb-ee92-4751-adc0-9c58d2fc78ae with an empty config and where the “component” is (snippet):

<!DOCTYPE html> <html lang="en"
data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" data-a11y-animated-images="system" data-a11y-link-underlines="true"
>


<head> <meta charset="utf-8"> <link rel="dns-prefetch" href="https://github.githubassets.com"> <link rel="dns-prefetch" href="https://avatars.githubusercontent.com"> <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com"> <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/"> <link rel="preconnect" href="https://github.githubassets.com" crossorigin> <link rel="preconnect" href="https://avatars.githubusercontent.com">



<link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light-7aa84bb7e11e.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark-f65db3e8d171.css" /><link data-color-theme="dark_dimmed" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_dimmed-a8258e3c6dda.css" /><link data-color-theme="dark_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_high_contrast-7e97d834719c.css" /><link data-color-theme="dark_colorblind" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_colorblind-01d869f460be.css" /><link data-color-theme="light_colorblind" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_colorblind-534f3e971240.css" /><link data-color-theme="light_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_high_contrast-a8cc7d138001.css" /><link data-color-theme="light_tritanopia" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_tritanopia-35e9dfdc4f9f.css" /><link data-color-theme="dark_tritanopia" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_tritanopia-cf4cc5f62dfe.css" />
<link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-primitives-d9abecd14f1e.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-93aded0ee8a1.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-1f2860c46060.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/github-a88fdad54119.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/repository-4fce88777fa8.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/code-0210be90f4d3.css" />



<script type="application/json" id="client-env">{"locale":"en","featureFlags":["bypass_copilot_indexing_quota","copilot_immersive_file_preview","copilot_new_references_ui","copilot_attach_folder_reference","copilot_chat_repo_custom_instructions_preview","copilot_chat_retry_on_error","copilot_chat_persist_submitted_input","copilot_conversational_ux_history_refs","copilot_chat_shared_topic_indicator","copilot_chat_shared_repo_sso_banner","copilot_editor_upsells","copilot_dotcom_chat_reduce_telemetry","copilot_free_limited_user","copilot_implicit_context","copilot_no_floating_button","copilot_smell_icebreaker_ux","copilot_new_markdown_renderer","experimentation_azure_variant_endpoint","failbot_handle_non_errors","geojson_azure_maps","ghost_pilot_confidence_truncation_25","ghost_pilot_confidence_truncation_40","github_models_o3_mini_streaming","github_models_per_chunk_timeout","hovercard_accessibility","issues_react_remove_placeholders","issues_react_blur_item_picker_on_close","issues_react_include_bots_in_pickers","marketing_pages_search_explore_provider","remove_child_patch","sample_network_conn_type","swp_enterprise_contact_form","site_copilot_acc","site_copilot_vscode_link_update","site_proxima_australia_update","issues_react_create_milestone","issues_react_cache_fix_workaround","lifecycle_label_name_updates"]}</script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/wp-runtime-3a3cb1987228.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_oddbird_popover-polyfill_dist_popover_js-9da652f58479.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_arianotify-polyfill_ariaNotify-polyfill_js-node_modules_github_mi-3abb8f-d7e6bc799724.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/ui_packages_failbot_failbot_ts-25697e0f4c47.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/environment-04ca94cb6e8a.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_primer_behaviors_dist_esm_index_mjs-0dbb79f97f8f.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_selector-observer_dist_index_esm_js-f690fd9ae3d5.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_relative-time-element_dist_index_js-f6da4b3fa34c.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_auto-complete-element_dist_index_js-node_modules_github_catalyst_-8e9f78-a74b4e0a8a6b.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_text-expander-element_dist_index_js-78748950cb0c.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_filter-input-element_dist_index_js-node_modules_github_remote-inp-b5f1d7-a1760ffda83d.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_markdown-toolbar-element_dist_index_js-ceef33f593fa.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_file-attachment-element_dist_index_js-node_modules_primer_view-co-c44a69-f0c8a795d1fd.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/github-elements-90f965d59632.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/element-registry-d018d1dc6e26.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_braintree_browser-detection_dist_browser-detection_js-node_modules_githu-bb80ec-72267f4e3ff9.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_lit-html_lit-html_js-be8cb88f481b.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_mini-throttle_dist_index_js-node_modules_morphdom_dist_morphdom-e-7c534c-a4a1922eb55f.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_turbo_dist_turbo_es2017-esm_js-e3cbe28f1638.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_remote-form_dist_index_js-node_modules_delegated-events_dist_inde-893f9f-6cf3320416b8.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_color-convert_index_js-e3180fe3bcb3.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_quote-selection_dist_index_js-node_modules_github_session-resume_-69cfcc-ccab506ecf8c.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/ui_packages_updatable-content_updatable-content_ts-439f48470426.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/app_assets_modules_github_behaviors_task-list_ts-app_assets_modules_github_sso_ts-ui_packages-900dde-03160297135f.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/app_assets_modules_github_sticky-scroll-into-view_ts-5316a27f9573.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/app_assets_modules_github_behaviors_ajax-error_ts-app_assets_modules_github_behaviors_include-87a4ae-0a6bb0ce2586.js"></script> <script crossorigin="anonymous" defer="defer" type="application/javascript" src="https://github.githubassets.com/assets/app_assets_modules

I wouldn’t think that should be possible, but I don’t know enough about how this works. Some type of validation in this step would be desirable though.