updated lovelace dashboards and migrate tcp sensors to use serial component with SOCAT

This commit is contained in:
root 2025-04-30 15:01:46 -04:00
parent 4999b85b4e
commit f4ceef3cfe
117 changed files with 56924 additions and 8281 deletions

View File

@ -1 +1 @@
2024.9.3 2025.4.4

4
.gitignore vendored
View File

@ -3,3 +3,7 @@
.DS_Store .DS_Store
secrets.yaml secrets.yaml
# pixi environments
.pixi
*.egg-info

View File

@ -16,9 +16,25 @@
"tts_language": null, "tts_language": null,
"tts_voice": null, "tts_voice": null,
"wake_word_entity": null, "wake_word_entity": null,
"wake_word_id": null "wake_word_id": null,
"prefer_local_intents": false
},
{
"conversation_engine": "01JCRKGQZQRNH78WSFVRBFXK98",
"conversation_language": "*",
"id": "01jcrc1n1z065tf45ds2a1z8rp",
"language": "en",
"name": "Home Assistant Cloud",
"stt_engine": "stt.home_assistant_cloud",
"stt_language": "en-US",
"tts_engine": "tts.home_assistant_cloud",
"tts_language": "en-US",
"tts_voice": "JennyNeural",
"wake_word_entity": null,
"wake_word_id": null,
"prefer_local_intents": true
} }
], ],
"preferred_item": "01h6w13v4kkqy9bdabx5qbrjy4" "preferred_item": "01jcrc1n1z065tf45ds2a1z8rp"
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
{
"version": 1,
"minor_version": 1,
"key": "browser_mod.storage",
"data": {
"browsers": {
"pecan-station": {
"last_seen": "2025-04-30T15:50:14.632453+00:00",
"registered": true,
"locked": true,
"camera": false,
"settings": {
"hideSidebar": null,
"hideHeader": null,
"defaultPanel": null,
"sidebarPanelOrder": null,
"sidebarHiddenPanels": null,
"sidebarTitle": null,
"faviconTemplate": null,
"titleTemplate": null,
"hideInteractIcon": null,
"autoRegister": null,
"lockRegister": null
},
"meta": "default"
},
"Ingest iPad": {
"last_seen": "2025-03-10T20:38:38.304530+00:00",
"registered": true,
"locked": true,
"camera": false,
"settings": {
"hideSidebar": null,
"hideHeader": null,
"defaultPanel": "dashboard-ingest",
"sidebarPanelOrder": null,
"sidebarHiddenPanels": null,
"sidebarTitle": null,
"faviconTemplate": null,
"titleTemplate": null,
"hideInteractIcon": null,
"autoRegister": null,
"lockRegister": null
},
"meta": "default"
}
},
"version": "2.0",
"settings": {
"hideSidebar": null,
"hideHeader": null,
"defaultPanel": null,
"sidebarPanelOrder": null,
"sidebarHiddenPanels": null,
"sidebarTitle": "USDA Assistant",
"faviconTemplate": "favicon/favicon.ico",
"titleTemplate": null,
"hideInteractIcon": null,
"autoRegister": null,
"lockRegister": null
},
"user_settings": {}
}
}

76
.storage/cloud Normal file
View File

@ -0,0 +1,76 @@
{
"version": 1,
"minor_version": 4,
"key": "cloud",
"data": {
"alexa_default_expose": [
"climate",
"cover",
"fan",
"humidifier",
"light",
"lock",
"scene",
"script",
"sensor",
"switch",
"vacuum",
"water_heater"
],
"alexa_entity_configs": {},
"alexa_settings_version": 3,
"cloud_user": "4ff51199e4ff4d96827eeef086468459",
"cloudhooks": {
"541b6ac0d1f592acf1d6dc8ff736c2bad653ee1e85a8b6409348c1eb974c6ce7": {
"webhook_id": "541b6ac0d1f592acf1d6dc8ff736c2bad653ee1e85a8b6409348c1eb974c6ce7",
"cloudhook_id": "df8dc4b010ad45f389819ad36aed7c5a",
"cloudhook_url": "https://hooks.nabu.casa/gAAAAABnN30j6_p0rowd_9HuWevBUbbMdFXr9Ub-Aqyo61X-_ZMoLCPDdnG7ZlnR4r8qSmvgZ2zDH8XkmDcJVeF57oj5iA_wUWnbX1TZepV5VSYPG_sKXUfPR80HyqGNhpkUhSlASFrShwZPF8JIeGdD-Z8a31ttjQgKg2KlxEZuyZ4flV0pNXo=",
"managed": true
},
"559877f352055fb8587094b48f394f450e316432bd9bc5e4fdb8fbaa00493e59": {
"webhook_id": "559877f352055fb8587094b48f394f450e316432bd9bc5e4fdb8fbaa00493e59",
"cloudhook_id": "54709579fe2b42859d77b2a03f8f6865",
"cloudhook_url": "https://hooks.nabu.casa/gAAAAABn02dCWUEMHlGjs-zCOuDsjWDWeCfAhcBBr2Uhg6-qo_5np8ypLA5vZRTe50YNVM187uxJFxg0Zh1HB83A0wC1j-i99MmdHOQ5511hHqazY7Q5pwRDZmfT4f-Qx0mZ169WX3m-39iw8hwgQiaD3rmk1FkeZbt2mcdXt802N6RUBf58zZ8=",
"managed": true
},
"7eb2a43c6447c831a473630a65e2f3c5f335de04f9192c0430bdd129576fbe7a": {
"webhook_id": "7eb2a43c6447c831a473630a65e2f3c5f335de04f9192c0430bdd129576fbe7a",
"cloudhook_id": "1f056b582cdd4b54841f09f5ec70d48b",
"cloudhook_url": "https://hooks.nabu.casa/gAAAAABoEkQ_BAwuMj2DgWl3Iwnyt6yX7ghlprYV2pnoAmz66JN_BxgZ4z1YRXswI1WFtiQarMOynEb0cJMrhxesr-cwaBt7HeYdVzZM-VzU1eRFzyzAq720QC66uoLzReVdFbQ21etyX1xiAJfvtgPPpazeyBuijkaPHMb88AqerBT31Y3Kuq4=",
"managed": true
},
"dd11c41f93a1ec05751fb1b2008d247fc3895548b22b21ddb7d013a02475264b": {
"webhook_id": "dd11c41f93a1ec05751fb1b2008d247fc3895548b22b21ddb7d013a02475264b",
"cloudhook_id": "de83cb4741374710aac2b1c0040a36fc",
"cloudhook_url": "https://hooks.nabu.casa/gAAAAABoEl4WK4dZP79V7lIO5-2iPtGyTUJBxH9pnlgwl086ztO6CogOHo2_VJPyGKGqF2r-98cTXFEzB854UtsDoJQzn-x8PP6dG9Ziz04LRwBWBQu9KN0fi1aFUNNri9jz7H2jjAKvkHsONDU5cADNvm-8NxiFgDT6U-e_kU6jhfug_LYak9I=",
"managed": true
}
},
"alexa_enabled": true,
"google_enabled": true,
"remote_enabled": true,
"google_connected": false,
"google_default_expose": [
"climate",
"cover",
"fan",
"humidifier",
"light",
"lock",
"scene",
"script",
"sensor",
"switch",
"vacuum",
"water_heater"
],
"google_entity_configs": {},
"google_settings_version": 3,
"google_local_webhook_id": "f4c5013efa8e06697da8be43dad0e9acaff71fea647951d6d6c38386a591bba1",
"instance_id": "4cf0780e04744cd69da58cb024b0c60b",
"google_secure_devices_pin": null,
"remote_domain": "wo1d5pxp3jix0xxkusbi44e44ztql5mu.ui.nabu.casa",
"remote_allow_remote_enable": true,
"username": "4361163a-c63e-4c39-84e4-8bce32ceb9fd"
}
}

View File

@ -1,41 +1,60 @@
{ {
"version": 1, "version": 1,
"minor_version": 7, "minor_version": 8,
"key": "core.area_registry", "key": "core.area_registry",
"data": { "data": {
"areas": [ "areas": [
{ {
"aliases": [], "aliases": [],
"name": "Living Room",
"id": "living_room",
"picture": null,
"icon": null,
"floor_id": null, "floor_id": null,
"humidity_entity_id": null,
"icon": null,
"id": "jc_machine",
"labels": [], "labels": [],
"created_at": "1970-01-01T00:00:00+00:00", "name": "JC Machine",
"modified_at": "1970-01-01T00:00:00+00:00" "picture": null,
"temperature_entity_id": null,
"created_at": "2025-03-08T19:12:57.095675+00:00",
"modified_at": "2025-03-08T19:12:57.095689+00:00"
}, },
{ {
"aliases": [], "aliases": [],
"name": "Kitchen",
"id": "kitchen",
"picture": null,
"icon": null,
"floor_id": null, "floor_id": null,
"humidity_entity_id": null,
"icon": null,
"id": "meyer_machine",
"labels": [], "labels": [],
"created_at": "1970-01-01T00:00:00+00:00", "name": "Meyer Machine",
"modified_at": "1970-01-01T00:00:00+00:00" "picture": null,
"temperature_entity_id": null,
"created_at": "2025-03-08T19:13:18.112708+00:00",
"modified_at": "2025-03-08T19:13:18.112824+00:00"
}, },
{ {
"aliases": [], "aliases": [],
"name": "Bedroom",
"id": "bedroom",
"picture": null,
"icon": null,
"floor_id": null, "floor_id": null,
"humidity_entity_id": null,
"icon": null,
"id": "sheller_machine",
"labels": [], "labels": [],
"created_at": "1970-01-01T00:00:00+00:00", "name": "Sheller Machine",
"modified_at": "1970-01-01T00:00:00+00:00" "picture": null,
"temperature_entity_id": null,
"created_at": "2025-03-08T19:13:22.445914+00:00",
"modified_at": "2025-03-08T19:13:22.445929+00:00"
},
{
"aliases": [],
"floor_id": null,
"humidity_entity_id": null,
"icon": null,
"id": "conditioning",
"labels": [],
"name": "Conditioning",
"picture": null,
"temperature_entity_id": null,
"created_at": "2025-03-08T19:13:27.588309+00:00",
"modified_at": "2025-03-08T19:13:27.588325+00:00"
} }
] ]
} }

View File

@ -1,644 +1,95 @@
{ {
"version": 1, "version": 1,
"minor_version": 3, "minor_version": 5,
"key": "core.config_entries", "key": "core.config_entries",
"data": { "data": {
"entries": [ "entries": [
{ {"created_at":"1970-01-01T00:00:00+00:00","data":{"broker":"192.168.1.110","port":1883},"disabled_by":null,"discovery_keys":{},"domain":"mqtt","entry_id":"143eb40c5189f32be0eddf773eaaeceb","minor_version":2,"modified_at":"2025-02-12T17:45:41.053251+00:00","options":{"birth_message":{"payload":"online","qos":0,"retain":false,"topic":"homeassistant/status"},"discovery":true,"discovery_prefix":"homeassistant","will_message":{"payload":"offline","qos":0,"retain":true,"topic":"homeassistant/status"}},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"192.168.1.110","unique_id":null,"version":1},
"created_at": "1970-01-01T00:00:00+00:00", {"created_at":"1970-01-01T00:00:00+00:00","data":{"host":"172.22.114.136","mac":"d0:03:df:ca:7c:74","model":"UN65RU8000FXZA","ssdp_rendering_control_location":"http://172.22.114.136:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"samsungtv","entry_id":"e2fde3af62ceb6eb0d4db0ce0395100e","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"Samsung TV 1048 Right (UN65RU8000FXZA)","unique_id":"8492f9dc-0f92-4ec9-951b-0d7e9c4918df","version":2},
"data": { {"created_at":"1970-01-01T00:00:00+00:00","data":{"host":"172.22.114.176","id":"Cloud Key Gen2 Plus","password":"1048Lab&2021","port":443,"username":"engr-ugaif","verify_ssl":false},"disabled_by":null,"discovery_keys":{},"domain":"unifiprotect","entry_id":"55db3b46f3bf75777e4779fd25ed6bca","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{"all_updates":false,"allow_ea":false,"disable_rtsp":false,"max_media":1000,"override_connection_host":false},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Cloud Key Gen2 Plus","unique_id":"70A741A53E33","version":2},
"broker": "192.168.1.110", {"created_at":"1970-01-01T00:00:00+00:00","data":{"type":"Setup as remote node"},"disabled_by":null,"discovery_keys":{},"domain":"remote_homeassistant","entry_id":"475e896c4033e0014ade6d0ef18b8329","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Remote instance","unique_id":"remote","version":1},
"port": 1883, {"created_at":"1970-01-01T00:00:00+00:00","data":{"device_id":"uuid:c73ad4db-6241-496a-80fd-994a43d1c980","mac":"d0:03:df:ca:7c:74","type":"urn:schemas-upnp-org:device:MediaRenderer:1","url":"http://172.22.114.136:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"dlna_dmr","entry_id":"d24486392bce9e68c96bb73e691681e4","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"[TV] Samsung TV 1048 Right","unique_id":"uuid:c73ad4db-6241-496a-80fd-994a43d1c980","version":1},
"discovery": true, {"created_at":"1970-01-01T00:00:00+00:00","data":{"device_id":"uuid:5cac41f8-5fa8-4d42-9c30-8c8b3f6050b0","mac":"24:fc:e5:5a:ff:68","type":"urn:schemas-upnp-org:device:MediaRenderer:1","url":"http://172.22.114.134:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"dlna_dmr","entry_id":"e172a44f6bd05f602b6469920fdc3754","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"[TV] Samsung TV 1048 Left","unique_id":"uuid:5cac41f8-5fa8-4d42-9c30-8c8b3f6050b0","version":1},
"discovery_prefix": "homeassistant", {"created_at":"1970-01-01T00:00:00+00:00","data":{"device_id":"uuid:8e7e5e53-46a1-4974-9513-05270ad0af77","mac":"24:fc:e5:5b:17:6a","type":"urn:schemas-upnp-org:device:MediaRenderer:1","url":"http://172.22.114.135:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"dlna_dmr","entry_id":"b02ec81df7205825511646b73cbb58f8","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"[TV] Samsung TV 1048 Center","unique_id":"uuid:8e7e5e53-46a1-4974-9513-05270ad0af77","version":1},
"birth_message": { {"created_at":"1970-01-01T00:00:00+00:00","data":{"host":"172.22.114.135","mac":"24:fc:e5:5b:17:6a","model":"UN65RU8000FXZA","ssdp_rendering_control_location":"http://172.22.114.135:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"samsungtv","entry_id":"6e73159a0788a6dbb3ccf4dc7a26945d","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"Samsung TV 1048 Center (UN65RU8000FXZA)","unique_id":"89947ca0-b33a-4912-b659-f4b1578a7777","version":2},
"topic": "homeassistant/status", {"created_at":"1970-01-01T00:00:00+00:00","data":{"host":"172.22.114.134","mac":"24:fc:e5:5a:ff:68","model":"UN65RU8000FXZA","ssdp_rendering_control_location":"http://172.22.114.134:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"samsungtv","entry_id":"5c87c2ce60943c4288dfd80aa9db5bf0","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"Samsung TV 1048 Left (UN65RU8000FXZA)","unique_id":"64bfe2c2-7b01-46b8-89d3-88c49373f55e","version":2},
"payload": "online", {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"remote_homeassistant","entry_id":"0200f22cdff3cef1905acfee956d71cc","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"Remote: Innovation Factory","unique_id":"311fbea761c143be9c14dc1cd5ab57b2","version":1},
"qos": 0, {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"sun","entry_id":"0feafcef6c9ee4eb380cad7190b2f403","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"import","subentries":[],"title":"Sun","unique_id":null,"version":1},
"retain": false {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"octoprint","entry_id":"7a20eaee207e2873f5296d198ac8dd63","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"OctoPrint Printer: 172.22.114.150","unique_id":"aef76126-1c40-459c-a460-ca7749222fdd","version":1},
}, {"created_at":"1970-01-01T00:00:00+00:00","data":{"app_data":{"push_token":"filDMYq4Sa2akEe_BlkVyg:APA91bHPxz2u6XjWrUIErPeakuxA-_VTCZT5JVa0vD5gkpe0P65aZhDC0Q6uNRQWJ2nQ3a6xRjI7uaB0ywup_WwSQNd8AkTqQlObEPr0AWO6ditliwFuh0EQWGQzU1rYFKiz7gq8zhBW","push_url":"https://mobile-apps.home-assistant.io/api/sendPush/android/v1","push_websocket_channel":true},"app_id":"io.homeassistant.companion.android","app_name":"Home Assistant","app_version":"2024.4.1-full (12576)","cloudhook_url":"https://hooks.nabu.casa/gAAAAABnN30j6_p0rowd_9HuWevBUbbMdFXr9Ub-Aqyo61X-_ZMoLCPDdnG7ZlnR4r8qSmvgZ2zDH8XkmDcJVeF57oj5iA_wUWnbX1TZepV5VSYPG_sKXUfPR80HyqGNhpkUhSlASFrShwZPF8JIeGdD-Z8a31ttjQgKg2KlxEZuyZ4flV0pNXo=","device_id":"2ddef885064fb8ed","device_name":"lab-phone","manufacturer":"samsung","model":"SM-A546U1","os_name":"Android","os_version":"34","supports_encryption":false,"user_id":"5ef2c8c082b14074a6e84da694ef2f35","webhook_id":"541b6ac0d1f592acf1d6dc8ff736c2bad653ee1e85a8b6409348c1eb974c6ce7"},"disabled_by":null,"discovery_keys":{},"domain":"mobile_app","entry_id":"e3427a6f1a531d4647c57351962f3e1a","minor_version":1,"modified_at":"2024-11-15T16:56:04.075371+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"registration","subentries":[],"title":"lab-phone","unique_id":"io.homeassistant.companion.android-2ddef885064fb8ed","version":1},
"will_message": { {"created_at":"1970-01-01T00:00:00+00:00","data":{"host":"172.22.113.18","mac":"70:09:71:0d:f6:5d","model":"UN50AU8000FXZA","ssdp_rendering_control_location":"http://172.22.113.18:9197/dmr"},"disabled_by":null,"discovery_keys":{},"domain":"samsungtv","entry_id":"54d38b0b04614f1d163c9ba14ce22fed","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"Samsung AU8000 50 TV (UN50AU8000FXZA)","unique_id":"7671ae54-70ac-426b-8fa0-a065cd4bd428","version":2},
"topic": "homeassistant/status", {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"mjpeg","entry_id":"f92d6ca163501ea7659047c60ba4e9e5","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{"authentication":"basic","mjpeg_url":"http://meyer:8080","password":"","still_image_url":null,"username":null,"verify_ssl":false},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Crack Output","unique_id":null,"version":1},
"payload": "offline", {"created_at":"1970-01-01T00:00:00+00:00","data":{"gen":2,"host":"192.168.1.152","model":"SNSW-001X15UL","port":80,"sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus1-cc7b5c0d0eb4._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus1-cc7b5c0d0eb4._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"04978edcf23c54a047e4f421779754ad","minor_version":1,"modified_at":"2025-03-11T14:17:43.900296+00:00","options":{"ble_scanner_mode":"passive"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Sheller Drum Enable","unique_id":"CC7B5C0D0EB4","version":1},
"qos": 0, {"created_at":"1970-01-01T00:00:00+00:00","data":{"gen":2,"host":"192.168.1.15","model":"SNSW-001X15UL","port":80,"sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus1-cc7b5c0d316c._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus1-cc7b5c0d316c._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"ce337fdb50b165d7ba080505d5c73343","minor_version":1,"modified_at":"2025-03-11T14:17:43.751327+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Sheller Paddle Shaft Enable","unique_id":"CC7B5C0D316C","version":1},
"retain": true {"created_at":"1970-01-01T00:00:00+00:00","data":{"gen":2,"host":"192.168.1.222","model":"SNDM-00100WW","port":80,"sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus010v-e86beae4d350._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus010v-e86beae4d350._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"779bd6f1f6eebd9fb67b45fa40386e0c","minor_version":1,"modified_at":"2025-03-11T14:17:43.885641+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Sheller Drum Velocity","unique_id":"E86BEAE4D350","version":1},
} {"created_at":"1970-01-01T00:00:00+00:00","data":{"gen":2,"host":"192.168.1.28","model":"SNDM-00100WW","sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus010v-e86beae4df24._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus010v-e86beae4df24._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"51355cd442e2d0c51a3a43811555ee77","minor_version":1,"modified_at":"2025-03-11T14:17:43.621631+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Sheller Paddle Shaft Velocity","unique_id":"E86BEAE4DF24","version":1},
}, {"created_at":"1970-01-01T00:00:00+00:00","data":{"alias":"TP-LINK_Power Strip_D7C1","connection_parameters":{"device_family":"IOT.SMARTPLUGSWITCH","encryption_type":"XOR","https":false},"host":"192.168.1.92","model":"HS300","uses_http":false},"disabled_by":null,"discovery_keys":{"dhcp":[{"domain":"dhcp","key":"98254af7d7c1","version":1}]},"domain":"tplink","entry_id":"37a922a368171d96e691a3439549d7bf","minor_version":5,"modified_at":"2025-01-07T17:42:09.326170+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"TP-LINK_Power Strip_D7C1 HS300(US)","unique_id":"98:25:4a:f7:d7:c1","version":1},
"disabled_by": null, {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"switch_as_x","entry_id":"e49f29f5d0e10a3ef63369b4c8c7f5c2","minor_version":2,"modified_at":"1970-01-01T00:00:00+00:00","options":{"entity_id":"switch.tp_link_power_strip_d7c1_light_2","invert":false,"target_domain":"light"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Light","unique_id":null,"version":1},
"domain": "mqtt", {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"switch_as_x","entry_id":"77ef41d0bbf25d6007b4b2968dc60f58","minor_version":2,"modified_at":"1970-01-01T00:00:00+00:00","options":{"entity_id":"switch.tp_link_power_strip_d7c1_light_3","invert":false,"target_domain":"light"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Light","unique_id":null,"version":1},
"entry_id": "143eb40c5189f32be0eddf773eaaeceb", {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"switch_as_x","entry_id":"54af0f5338887810d55839a908975bd3","minor_version":2,"modified_at":"1970-01-01T00:00:00+00:00","options":{"entity_id":"switch.tp_link_power_strip_d7c1_light_4","invert":false,"target_domain":"light"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Light ","unique_id":null,"version":1},
"minor_version": 1, {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"group","entry_id":"7451d239431f2b6ea2ee2a1b85ab5c56","minor_version":1,"modified_at":"1970-01-01T00:00:00+00:00","options":{"all":false,"entities":["light.tp_link_power_strip_d7c1_light","light.tp_link_power_strip_d7c1_light_2","light.tp_link_power_strip_d7c1_light_3","light.tp_link_power_strip_d7c1_light_4"],"group_type":"light","hide_members":false,"name":"Conveyor Lights"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Conveyor Lights","unique_id":null,"version":1},
"modified_at": "2024-09-30T18:02:15.200209+00:00", {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"switch_as_x","entry_id":"82a8d9eef25b35d19d72cd6ce5f9dcbe","minor_version":2,"modified_at":"1970-01-01T00:00:00+00:00","options":{"entity_id":"switch.tp_link_power_strip_d7c1_light","invert":false,"target_domain":"light"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Light ","unique_id":null,"version":1},
"options": {}, {"created_at":"1970-01-01T00:00:00+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"switch_as_x","entry_id":"f1e37a42d2b569eaa4ac24c20a31fa24","minor_version":2,"modified_at":"1970-01-01T00:00:00+00:00","options":{"entity_id":"switch.tp_link_power_strip_d7c1_zima_board","invert":false,"target_domain":"light"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Vibratory Lights","unique_id":null,"version":1},
"pref_disable_new_entities": false, {"created_at":"1970-01-01T00:00:00+00:00","data":{"gen":2,"host":"192.168.1.139","model":"SNSW-001P15UL","port":80,"sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus1pm-c049ef8c7310._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus1pm-c049ef8c7310._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"01J5NH0A41TYMRRGASE2QJ0SQ5","minor_version":2,"modified_at":"2025-03-10T20:01:44.621850+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"JC Feed","unique_id":"C049EF8C7310","version":1},
"pref_disable_polling": false, {"created_at":"2024-09-30T14:21:49.515314+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"hassio","entry_id":"01J91MY5JBJH64GQS4WA5SYVK2","minor_version":1,"modified_at":"2024-09-30T14:21:49.515328+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"system","subentries":[],"title":"Supervisor","unique_id":"hassio","version":1},
"source": "user", {"created_at":"2024-10-22T13:58:20.469378+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"browser_mod","entry_id":"01JAT8AZHNSF6WPATHNE5D6XM4","minor_version":1,"modified_at":"2024-10-22T13:58:20.469391+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Browser Mod","unique_id":null,"version":2},
"title": "192.168.1.110", {"created_at":"2024-11-06T18:40:57.619153+00:00","data":{"alias":"Vibratory Conveyor","connection_parameters":{"device_family":"IOT.SMARTPLUGSWITCH","encryption_type":"XOR","https":false},"host":"192.168.1.107","model":"KP115","uses_http":false},"disabled_by":null,"discovery_keys":{"dhcp":[{"domain":"dhcp","key":"54af970993f8","version":1}]},"domain":"tplink","entry_id":"01JC1CF88K7YTRGZT79CYQS536","minor_version":5,"modified_at":"2025-01-07T17:42:07.946491+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"dhcp","subentries":[],"title":"Blower Lights","unique_id":"54:af:97:09:93:f8","version":1},
"unique_id": null, {"created_at":"2024-11-11T21:11:20.531392+00:00","data":{"gen":2,"host":"192.168.1.75","model":"SNSW-001X15UL","sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","ShellyPlus1-B8D61A87D2A8._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus1-b8d61a87d2a8._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus1-b8d61a87d2a8._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"01JCEH26PKQ5WZ0GQ495DCG9WM","minor_version":2,"modified_at":"2025-03-10T20:01:44.645332+00:00","options":{"ble_scanner_mode":"disabled"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"zeroconf","subentries":[],"title":"Bucket Elevator","unique_id":"B8D61A87D2A8","version":1},
"version": 1 {"created_at":"2024-11-11T21:11:23.987940+00:00","data":{"gen":2,"host":"192.168.1.7","model":"SNSW-001X15UL","sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","ShellyPlus1-B8D61A8A7508._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus1-b8d61a8a7508._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus1-b8d61a8a7508._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"01JCEH2A2K8Y9VEK9XV8ZPC69H","minor_version":2,"modified_at":"2025-03-10T20:01:44.614068+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"zeroconf","subentries":[],"title":"Sheller Feed","unique_id":"B8D61A8A7508","version":1},
}, {"created_at":"2024-11-15T16:56:03.079266+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"cloud","entry_id":"01JCRC1M87G227CQKT0Z1J30HB","minor_version":1,"modified_at":"2024-11-15T16:56:03.079277+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"system","subentries":[],"title":"Home Assistant Cloud","unique_id":null,"version":1},
{ {"created_at":"2024-11-15T19:06:38.455479+00:00","data":{"api_key":"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","name":"FILLIS","skip_authentication":false},"disabled_by":null,"discovery_keys":{},"domain":"extended_openai_conversation","entry_id":"01JCRKGQZQRNH78WSFVRBFXK98","minor_version":1,"modified_at":"2024-11-18T20:51:09.471467+00:00","options":{"attach_username":false,"chat_model":"gpt-4o-mini","context_threshold":13000,"context_truncate_strategy":"clear","functions":"- spec:\n name: execute_services\n description: Use this function to execute service of devices in Home Assistant.\n parameters:\n type: object\n properties:\n list:\n type: array\n items:\n type: object\n properties:\n domain:\n type: string\n description: The domain of the service\n service:\n type: string\n description: The service to be called\n service_data:\n type: object\n description: The service data object to indicate what to control.\n properties:\n entity_id:\n type: string\n description: The entity_id retrieved from available devices. It\n must start with domain, followed by dot character.\n required:\n - entity_id\n required:\n - domain\n - service\n - service_data\n function:\n type: native\n name: execute_service","max_function_calls_per_conversation":2,"max_tokens":150,"prompt":"I want you to act as smart home manager of Home Assistant.\nI will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language.\n\nCurrent Time: {{now()}}\n\nAvailable Devices:\n```csv\nentity_id,name,state,aliases\n{% for entity in exposed_entities -%}\n{{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{entity.aliases | join('/')}}\n{% endfor -%}\n```\n\nThe current state of devices is provided in available devices.\nUse execute_services function only for requested action, not for current states.\nOnly ask for clarification before executing a task if the request is ambiguous.\nDo not restate or appreciate what user says, rather make a quick inquiry.","temperature":0.5,"top_p":1.0,"use_tools":false},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"FILLIS","unique_id":null,"version":1},
"created_at": "1970-01-01T00:00:00+00:00", {"created_at":"2024-11-18T15:35:15.370437+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"go2rtc","entry_id":"01JCZYKV5A7YB4FK4GZKHJ40FR","minor_version":1,"modified_at":"2024-11-18T15:35:15.370466+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"system","subentries":[],"title":"go2rtc","unique_id":null,"version":1},
"data": { {"created_at":"2024-11-18T20:44:07.871851+00:00","data":{"gen":2,"host":"192.168.1.213","model":"SNDM-00100WW","sleep_period":0},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_http._tcp.local.","shellyplus010v-e86beae47374._http._tcp.local."],"version":1},{"domain":"zeroconf","key":["_shelly._tcp.local.","shellyplus010v-e86beae47374._shelly._tcp.local."],"version":1}]},"domain":"shelly","entry_id":"01JD0G9D9ZXBC64EXQHJXE9AEQ","minor_version":2,"modified_at":"2025-03-10T20:01:44.625876+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"zeroconf","subentries":[],"title":"JC Feed Rate","unique_id":"E86BEAE47374","version":1},
"host": "172.22.114.136", {"created_at":"2024-11-20T15:58:18.232682+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JD54QFKRXGN1QSXE9X70JPAE","minor_version":1,"modified_at":"2024-11-20T15:58:18.232707+00:00","options":{"name":"Drum RPM Error","state":"{% set setpoint = states('input_number.sheller_drum_rpm') | float %}\n{% set current_rpm = states('sensor.shelling_machine_drum_rpm') | float %}\n{{ setpoint - current_rpm }}","template_type":"sensor","unit_of_measurement":"RPM"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Drum RPM Error","unique_id":null,"version":1},
"mac": "d0:03:df:ca:7c:74", {"created_at":"2024-11-20T16:00:09.173007+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"derivative","entry_id":"01JD54TVYMSG343PEB5S3QEE5Q","minor_version":1,"modified_at":"2024-11-20T16:00:09.173032+00:00","options":{"name":"Drum RPM Error Derivative","round":2.0,"source":"sensor.drum_rpm_error","time_window":{"hours":0,"minutes":0,"seconds":0},"unit_time":"s"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Drum RPM Error Derivative","unique_id":null,"version":1},
"model": "UN65RU8000FXZA", {"created_at":"2024-11-20T16:00:54.272576+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"integration","entry_id":"01JD54W800M5NH8WGPEGQR8Z4M","minor_version":1,"modified_at":"2024-11-20T17:13:04.246060+00:00","options":{"method":"trapezoidal","name":"Drum RPM Error Integral","round":2.0,"source":"sensor.drum_rpm_error","unit_time":"s"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Drum RPM Error Integral","unique_id":null,"version":1},
"ssdp_rendering_control_location": "http://172.22.114.136:9197/dmr" {"created_at":"2024-11-25T19:54:08.723604+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JDJE6XEKP4RTRT04EWJXBKMC","minor_version":1,"modified_at":"2024-11-25T19:54:08.723627+00:00","options":{"name":"Paddle RPM Error","state":"{% set setpoint = states('input_number.sheller_paddle_rpm') | float %}\n{% set current_rpm = states('sensor.shelling_machine_paddle_rpm') | float %}\n{{ setpoint - current_rpm }}","template_type":"sensor"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Paddle RPM Error","unique_id":null,"version":1},
}, {"created_at":"2025-02-12T17:45:59.457632+00:00","data":{"source":"CC:7B:5C:0D:0E:B6","source_config_entry_id":"04978edcf23c54a047e4f421779754ad","source_device_id":"98bc66339afcb5b0ee18bd93df9ab0f0","source_domain":"shelly","source_model":"SNSW-001X15UL"},"disabled_by":null,"discovery_keys":{},"domain":"bluetooth","entry_id":"01JKXM91D1BNRC2505EHYRESCW","minor_version":1,"modified_at":"2025-04-08T16:25:25.578111+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"integration_discovery","subentries":[],"title":"Sheller Drum Enable (CC:7B:5C:0D:0E:B4)","unique_id":"CC:7B:5C:0D:0E:B6","version":1},
"disabled_by": null, {"created_at":"2025-02-12T17:52:17.940035+00:00","data":{"token":"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"},"disabled_by":null,"discovery_keys":{},"domain":"hacs","entry_id":"01JKXMMK0K5E1EZJ90XJCAKPBB","minor_version":1,"modified_at":"2025-02-25T21:28:58.275201+00:00","options":{"appdaemon":false,"country":"ALL","experimental":true,"sidepanel_icon":"hacs:hacs","sidepanel_title":"HACS"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"","unique_id":null,"version":1},
"domain": "samsungtv", {"created_at":"2025-02-19T15:40:35.874625+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JMFDWF52FJ40CHPD6N1F3CQ2","minor_version":1,"modified_at":"2025-02-19T15:40:35.874647+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"6E51E56D","version":1},
"entry_id": "e2fde3af62ceb6eb0d4db0ce0395100e", {"created_at":"2025-02-20T19:16:00.924282+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"derivative","entry_id":"01JMJCKM8W4AC2KEN3RA72QVJ5","minor_version":1,"modified_at":"2025-02-20T20:21:23.307948+00:00","options":{"name":"JC Throughput Rate","round":2.0,"source":"input_number.jc_rolling_pecan_sum","time_window":{"hours":0,"minutes":0,"seconds":5},"unit_time":"s"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"JC Throughput Rate","unique_id":null,"version":1},
"minor_version": 1, {"created_at":"2025-02-20T20:29:05.834521+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JMJGSEDAV5NQ29SSFY191FMH","minor_version":1,"modified_at":"2025-02-20T20:39:22.960233+00:00","options":{"name":"JC Filtered Pecan Rate","state":"{% set raw_value = states('sensor.raw_derivative_sensor') | float(0) %}\n{% set previous = states('sensor.smoothed_derivative_sensor') | float(0) %}\n{% set alpha = 0.3 %}\n \n{% if raw_value >= 0 %}\n {{ (alpha * raw_value) + ((1 - alpha) * previous) }}\n{% else %}\n {{ 0 }}\n{% endif %}","state_class":"measurement","template_type":"sensor","unit_of_measurement":"Pecans/s"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"JC Filtered Pecan Rate","unique_id":null,"version":1},
"modified_at": "1970-01-01T00:00:00+00:00", {"created_at":"2025-02-20T20:42:12.675265+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JMJHHET3YTENCCSCX905TPBK","minor_version":1,"modified_at":"2025-02-20T20:42:12.675294+00:00","options":{"name":"JC Pecan Rate Error","state":"{% set setpoint = states('number.jc_feedrate_setpoint') | float %}\n{% set current_rate = states('sensor.jc_filtered_pecan_rate') | float %}\n{{ current_rate - setpoint }}","template_type":"sensor"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"JC Pecan Rate Error","unique_id":null,"version":1},
"options": {}, {"created_at":"2025-03-08T19:26:10.104581+00:00","data":{},"disabled_by":null,"discovery_keys":{"zeroconf":[{"domain":"zeroconf","key":["_home-assistant._tcp.local.","Home._home-assistant._tcp.local."],"version":1}]},"domain":"remote_homeassistant","entry_id":"01JNVKHQ5R3524AF4YR7JR1NY5","minor_version":1,"modified_at":"2025-03-08T19:26:10.104609+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"Remote: Home","unique_id":"bc2f715b079245fa8e2ed0f159eddf29","version":1},
"pref_disable_new_entities": false, {"created_at":"2025-03-08T22:30:06.708904+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JNVY2H3M56KRM6T10PQYW5PQ","minor_version":1,"modified_at":"2025-03-08T22:30:06.708934+00:00","options":{"name":"MoistTech Whole Moisture","state":"{% set values = states('sensor.whole_pecan_moisture') | default('0,0,0') | regex_findall('[\\\\d\\\\.\\\\-]+') %}\n{{ values[0] | float }}","template_type":"sensor","unit_of_measurement":"%"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"MoistTech Whole Moisture","unique_id":null,"version":1},
"pref_disable_polling": false, {"created_at":"2025-03-08T22:30:33.745961+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JNVY3BGHH66319YRTB3VDH82","minor_version":1,"modified_at":"2025-03-08T22:30:33.745987+00:00","options":{"name":"MoistTech Moisture Shell","state":"{% set values = states('sensor.whole_pecan_moisture') | default('0,0,0') | regex_findall('[\\\\d\\\\.\\\\-]+') %}\n{{ values[1] | float }}","template_type":"sensor","unit_of_measurement":"%"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"MoistTech Moisture Shell","unique_id":null,"version":1},
"source": "ignore", {"created_at":"2025-03-08T22:31:28.316229+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JNVY50SWV9FAMK7H3BTJW7KP","minor_version":1,"modified_at":"2025-03-08T22:31:28.316257+00:00","options":{"name":"MoistTech Kernel Moisture","state":"{% set values = states('sensor.whole_pecan_moisture') | default('0,0,0') | regex_findall('[\\\\d\\\\.\\\\-]+') %}\n{{ values[2] | float }}","template_type":"sensor","unit_of_measurement":"%"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"MoistTech Kernel Moisture","unique_id":null,"version":1},
"title": "Samsung TV 1048 Right (UN65RU8000FXZA)", {"created_at":"2025-03-10T20:45:49.983562+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JP0WX10Z7ERWC9SAC548A5FP","minor_version":1,"modified_at":"2025-03-10T20:45:49.983576+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"1ADE4304","version":1},
"unique_id": "8492f9dc-0f92-4ec9-951b-0d7e9c4918df", {"created_at":"2025-03-11T13:48:28.798268+00:00","data":{"device_modification":{"attribute_modification":{"hw_version":null,"manufacturer":"ME&E","model":"JC","serial_number":null,"sw_version":null,"via_device_id":null},"device_id":"bd08feb9232d2cb49bb7742ac8a6bd13","device_name":"JC Cracker","entity_modification":{"entities":["f2132fd3b40e6062062421b5a7923067","177cd51d18be8978f0f3d3c03bd134e4","d6b01587f9fde6993eae701d85df4063","50f517b1e53905454a1ba663291b729c","6110c4cf3290251b791ac3175ba5302d","73830ed5b94ac16da41f23dcc0fef8f3","874e4cc6e772d9a0d2108f48ed8b9699","f5e4c06d08c43feb1cbebc59953d9cac","4e995cc33d0cacb73083be9cca041951","bb4a01566216cd351e20e219d612e636","adda4a1f4f0e15dd6990542bd6745645","95036a4e7dbf623a4c77bcb9af3776ba","5ffc341caf05e300ac095fdf698ba697","4e483e0723ab1f29bc6a94d655c81eef"]},"merge_modification":null,"modification_name":"JC Cracker Mod"}},"disabled_by":null,"discovery_keys":{},"domain":"device_tools","entry_id":"01JP2QDHQY8CAXX25PYRGQRD4E","minor_version":1,"modified_at":"2025-03-11T14:43:57.478239+00:00","options":{"device_modification":{"attribute_modification":{"hw_version":null,"manufacturer":"ME&E","model":"JC","serial_number":null,"sw_version":null,"via_device_id":null},"device_id":"bd08feb9232d2cb49bb7742ac8a6bd13","device_name":"JC Cracker","entity_modification":{"entities":["f2132fd3b40e6062062421b5a7923067","177cd51d18be8978f0f3d3c03bd134e4","d6b01587f9fde6993eae701d85df4063","50f517b1e53905454a1ba663291b729c","6110c4cf3290251b791ac3175ba5302d","73830ed5b94ac16da41f23dcc0fef8f3","874e4cc6e772d9a0d2108f48ed8b9699","f5e4c06d08c43feb1cbebc59953d9cac","4e995cc33d0cacb73083be9cca041951","bb4a01566216cd351e20e219d612e636","adda4a1f4f0e15dd6990542bd6745645","95036a4e7dbf623a4c77bcb9af3776ba","5ffc341caf05e300ac095fdf698ba697","4e483e0723ab1f29bc6a94d655c81eef"]},"merge_modification":null,"modification_name":"JC Cracker Mod"}},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"JC Cracker Mod","unique_id":"JC Cracker Mod","version":1},
"version": 2 {"created_at":"2025-03-11T14:12:49.100627+00:00","data":{"device_modification":{"attribute_modification":null,"device_id":"7fb5d449ee7e9b4b92969c8006ef3f8d","device_name":"Meyer Cracker","entity_modification":{"entities":["d18878bb83949542c3c0fa3b67c8a577","162d5c89569e4fac2fa1fc994ec82e4e","3a9812ef5656d9c3cfc3759dfe9dcda0","f2c8299058756338f530143024f54206","c357504fff9a1920c4dc8a876d864cb3","279277d53631d3556b3aacb5d08ba9c2"]},"merge_modification":null,"modification_name":"Meyer Cracker Mod"}},"disabled_by":null,"discovery_keys":{},"domain":"device_tools","entry_id":"01JP2RT3TC42WQMQX86W8PBYGR","minor_version":1,"modified_at":"2025-03-11T14:42:37.662757+00:00","options":{"device_modification":{"attribute_modification":null,"device_id":"7fb5d449ee7e9b4b92969c8006ef3f8d","device_name":"Meyer Cracker","entity_modification":{"entities":["d18878bb83949542c3c0fa3b67c8a577","162d5c89569e4fac2fa1fc994ec82e4e","3a9812ef5656d9c3cfc3759dfe9dcda0","f2c8299058756338f530143024f54206","c357504fff9a1920c4dc8a876d864cb3","279277d53631d3556b3aacb5d08ba9c2"]},"merge_modification":null,"modification_name":"Meyer Cracker Mod"}},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Meyer Cracker Mod","unique_id":"Meyer Cracker Mod","version":1},
}, {"created_at":"2025-03-11T14:15:19.168578+00:00","data":{"device_modification":{"attribute_modification":null,"device_id":"ac5aeecdb81f624f67d67ca7cd25d696","device_name":"Sheller Machine","entity_modification":{"entities":["2cd4708456a3e3c5d5b3707932710e28","efb0d43f0df4c3808b5618515f541cb2","c0cbbd4924f42c0c46871a488926eda1","a2f91fbc44eb6159ee0469a3632a7052","65fcc6ffbfb2be888ad0056cb8753788","8ed34b95c20b69e1ade8b05baae878c5","5a9599d2c47a92f788ce46aa28c791a7","772136a67a3a1a1c1a68b3912e3437ff","dd00b743411562e281695d49f5c9b578","10246760e6f40898d03630c23f9bfa5f"]},"merge_modification":{"devices":[]},"modification_name":"Sheller Machine Mod"}},"disabled_by":null,"discovery_keys":{},"domain":"device_tools","entry_id":"01JP2RYPC08P80S3KGJGNQTG5B","minor_version":1,"modified_at":"2025-03-11T14:39:46.389364+00:00","options":{"device_modification":{"attribute_modification":null,"device_id":"ac5aeecdb81f624f67d67ca7cd25d696","device_name":"Sheller Machine","entity_modification":{"entities":["2cd4708456a3e3c5d5b3707932710e28","efb0d43f0df4c3808b5618515f541cb2","c0cbbd4924f42c0c46871a488926eda1","a2f91fbc44eb6159ee0469a3632a7052","65fcc6ffbfb2be888ad0056cb8753788","8ed34b95c20b69e1ade8b05baae878c5","5a9599d2c47a92f788ce46aa28c791a7","772136a67a3a1a1c1a68b3912e3437ff","dd00b743411562e281695d49f5c9b578","10246760e6f40898d03630c23f9bfa5f"]},"merge_modification":{"devices":[]},"modification_name":"Sheller Machine Mod"}},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Sheller Machine Mod","unique_id":"Sheller Machine Mod","version":1},
{ {"created_at":"2025-04-08T16:25:33.648397+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"backup","entry_id":"01JRB3H9PGGBS0Y87YRVZFBN7S","minor_version":1,"modified_at":"2025-04-08T16:25:33.648399+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"system","subentries":[],"title":"Backup","unique_id":null,"version":1},
"created_at": "1970-01-01T00:00:00+00:00", {"created_at":"2025-04-17T15:56:20.036958+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"bluetooth","entry_id":"01JS27E8649HW777MJ0KF33QA2","minor_version":1,"modified_at":"2025-04-17T15:56:57.281299+00:00","options":{"passive":true},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"integration_discovery","subentries":[],"title":"Realtek Bluetooth Radio (58:10:31:E7:7C:01)","unique_id":"58:10:31:E7:7C:01","version":1},
"data": { {"created_at":"2025-04-17T15:57:10.033911+00:00","data":{},"disabled_by":null,"discovery_keys":{"bluetooth":[{"domain":"bluetooth","key":"FB:AC:C2:D9:08:D7","version":1}]},"domain":"ibeacon","entry_id":"01JS27FS0HQ474CTAVCV1DQ60H","minor_version":1,"modified_at":"2025-04-17T15:57:10.033912+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"bluetooth","subentries":[],"title":"iBeacon Tracker","unique_id":null,"version":1},
"host": "192.168.1.192", {"created_at":"2025-04-17T19:42:42.404412+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2MCR74A3D7PYB2M2S8ZA3D","minor_version":1,"modified_at":"2025-04-17T19:42:42.404413+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"810112472622","version":1},
"port": 6053, {"created_at":"2025-04-17T19:57:08.407019+00:00","data":{"user":"7e6db422e3a54f0982d4375af0f6074d"},"disabled_by":null,"discovery_keys":{},"domain":"voip","entry_id":"01JS2N75XQZ4PBCK7CDCZ1X2KP","minor_version":1,"modified_at":"2025-04-17T19:57:14.572733+00:00","options":{"sip_port":5060},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","subentries":[],"title":"Voice over IP","unique_id":null,"version":1},
"password": "", {"created_at":"2025-04-17T21:32:55.104514+00:00","data":{"host":"192.168.1.1","location":"http://192.168.1.1:34943/rootDesc.xml","mac_address":"f4:e2:c6:e4:d2:f6","original_udn":"uuid:8d6c30fd-0475-44b8-9c12-466689f35e58","st":"urn:schemas-upnp-org:device:InternetGatewayDevice:2","udn":"uuid:8d6c30fd-0475-44b8-9c12-466689f35e58"},"disabled_by":null,"discovery_keys":{"ssdp":[{"domain":"ssdp","key":"uuid:8d6c30fd-0475-44b8-9c12-466689f35e58","version":1}]},"domain":"upnp","entry_id":"01JS2TPHY0WM1EHSFM6PP3N93A","minor_version":1,"modified_at":"2025-04-17T21:32:55.104515+00:00","options":{"force_poll":false},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ssdp","subentries":[],"title":"UniFi NeXt-Gen Gateway","unique_id":"uuid:8d6c30fd-0475-44b8-9c12-466689f35e58::urn:schemas-upnp-org:device:InternetGatewayDevice:2","version":1},
"noise_psk": "OSntu1sSvgNHlNbyi9VYm0somCYIF2bOu11U2ckotX8=", {"created_at":"2025-04-17T21:33:11.568575+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQ20GKXGYZ7JEGGG77H56","minor_version":1,"modified_at":"2025-04-17T21:33:11.568576+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"723430","version":1},
"device_name": "jc-vibratory" {"created_at":"2025-04-17T21:33:14.176498+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQ4J0P53CKM97DHMTCFM0","minor_version":1,"modified_at":"2025-04-17T21:33:14.176499+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"732159","version":1},
}, {"created_at":"2025-04-17T21:33:23.136423+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQDA0RBZBDCTM7K6WPP5J","minor_version":1,"modified_at":"2025-04-17T21:33:23.136424+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"1B983A04","version":1},
"disabled_by": null, {"created_at":"2025-04-17T21:33:24.624344+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQERG25GFR7SNPNS1MT3H","minor_version":1,"modified_at":"2025-04-17T21:33:24.624345+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"1145FB04","version":1},
"domain": "esphome", {"created_at":"2025-04-17T21:33:27.232648+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQHA0E4YGP66KZFCF389C","minor_version":1,"modified_at":"2025-04-17T21:33:27.232649+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500033","version":1},
"entry_id": "c0c49687c85dee58eea3dbb414dd9337", {"created_at":"2025-04-17T21:33:28.592388+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQJMGVXHYK65EQNZN18C6","minor_version":1,"modified_at":"2025-04-17T21:33:28.592389+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500021","version":1},
"minor_version": 1, {"created_at":"2025-04-17T21:33:30.800513+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQMSGNZ3G13Q16GXK3SFC","minor_version":1,"modified_at":"2025-04-17T21:33:30.800514+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"043a981b6f6180","version":1},
"modified_at": "1970-01-01T00:00:00+00:00", {"created_at":"2025-04-17T21:33:32.368316+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQPAGWAP6KHMVJYC0W8BN","minor_version":1,"modified_at":"2025-04-17T21:33:32.368317+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"0443de1a6f6180","version":1},
"options": { {"created_at":"2025-04-17T21:33:34.008690+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQQXR29E050QN9TZQD3AV","minor_version":1,"modified_at":"2025-04-17T21:33:34.008692+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"04e56c11bb2a81","version":1},
"allow_service_calls": false {"created_at":"2025-04-17T21:33:35.488279+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQSC0GMMT7GP8Z2D2M998","minor_version":1,"modified_at":"2025-04-17T21:33:35.488280+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"10F61E04","version":1},
}, {"created_at":"2025-04-17T21:33:36.857493+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQTPSQKFSDZX6JRAER67E","minor_version":1,"modified_at":"2025-04-17T21:33:36.857494+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"041ef610bb2a81","version":1},
"pref_disable_new_entities": false, {"created_at":"2025-04-17T21:33:38.440624+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQW88QP7XAK0158CHX2MJ","minor_version":1,"modified_at":"2025-04-17T21:33:38.440625+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"10f61e04","version":1},
"pref_disable_polling": false, {"created_at":"2025-04-17T21:33:39.816285+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQXK8MN52BQ9BKFBGZ8XV","minor_version":1,"modified_at":"2025-04-17T21:33:39.816286+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500032","version":1},
"source": "user", {"created_at":"2025-04-17T21:33:41.320433+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TQZ28C3Q08VZ5QK3R5756","minor_version":1,"modified_at":"2025-04-17T21:33:41.320434+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"1A4E4B04","version":1},
"title": "jc-vibratory", {"created_at":"2025-04-17T21:33:42.616562+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR0ARGD97RSD6MRAF8JX4","minor_version":1,"modified_at":"2025-04-17T21:33:42.616563+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500004","version":1},
"unique_id": "60:01:94:cf:79:15", {"created_at":"2025-04-17T21:33:44.040385+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR1Q8WM1GCQFKMREK14ZH","minor_version":1,"modified_at":"2025-04-17T21:33:44.040386+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500005","version":1},
"version": 1 {"created_at":"2025-04-17T21:33:45.472517+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR340Z650GGJP852RV0ZD","minor_version":1,"modified_at":"2025-04-17T21:33:45.472518+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500001","version":1},
}, {"created_at":"2025-04-17T21:33:47.088485+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR4PG97Q2527YW6RNN4Q2","minor_version":1,"modified_at":"2025-04-17T21:33:47.088486+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500012","version":1},
{ {"created_at":"2025-04-17T21:33:48.608462+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR6601PPQHECZJVSYFB5P","minor_version":1,"modified_at":"2025-04-17T21:33:48.608463+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500011","version":1},
"created_at": "1970-01-01T00:00:00+00:00", {"created_at":"2025-04-17T21:33:50.040439+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR7JRZKJXRZRC63T45HWD","minor_version":1,"modified_at":"2025-04-17T21:33:50.040440+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"500002","version":1},
"data": { {"created_at":"2025-04-17T21:33:51.296396+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TR8T0SFRG9ED3C172MDP2","minor_version":1,"modified_at":"2025-04-17T21:33:51.296398+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"17D18B04","version":1},
"host": "172.22.114.176", {"created_at":"2025-04-17T21:33:52.856555+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TRAARAK274K6XCC26415A","minor_version":1,"modified_at":"2025-04-17T21:33:52.856556+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"730494","version":1},
"port": 443, {"created_at":"2025-04-17T21:33:54.160598+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TRBKG38583WTPDVRG0Q67","minor_version":1,"modified_at":"2025-04-17T21:33:54.160599+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"732981","version":1},
"verify_ssl": false, {"created_at":"2025-04-17T21:33:55.568345+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TRCZGCP0AYDHYJV7SE6DN","minor_version":1,"modified_at":"2025-04-17T21:33:55.568346+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"740351","version":1},
"username": "engr-ugaif", {"created_at":"2025-04-17T21:33:57.024553+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"rfid_batches","entry_id":"01JS2TRED04QM3WRVZ32M081MW","minor_version":1,"modified_at":"2025-04-17T21:33:57.024555+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"ignore","subentries":[],"title":"RFID Batches","unique_id":"739763","version":1},
"password": "1048Lab&2021", {"created_at":"2025-04-30T17:29:58.664979+00:00","data":{"app_data":{"push_token":"evqH9xXBckNTlIiTPSjtBY:APA91bGOQ_hCyWDl2oEfgFcpOnCF_kciPJodnJ2uBUxSIDIoCjew5281_MOSag3Cs0Jl1ongRwDy3-fA-RmIMQEpWSfHk8K0PPPMisKhk0vt-AUkTQ0z6E0","push_url":"https://mobile-apps.home-assistant.io/api/sendPushNotification"},"app_id":"io.robbie.HomeAssistant","app_name":"Home Assistant","app_version":"2025.2 (2025.1178)","cloudhook_url":"https://hooks.nabu.casa/gAAAAABoEl4WK4dZP79V7lIO5-2iPtGyTUJBxH9pnlgwl086ztO6CogOHo2_VJPyGKGqF2r-98cTXFEzB854UtsDoJQzn-x8PP6dG9Ziz04LRwBWBQu9KN0fi1aFUNNri9jz7H2jjAKvkHsONDU5cADNvm-8NxiFgDT6U-e_kU6jhfug_LYak9I=","device_id":"98A9293F-CB61-45EC-B9B6-192CF169CDAD","device_name":"Factorys iPad","manufacturer":"Apple","model":"iPad12,1","no_legacy_encryption":true,"os_name":"iPadOS","os_version":"15.5","secret":"ff886fbce70b2431093a0913ec13393608930b3521e9bafe27761be28655fee3","supports_encryption":true,"user_id":"5ef2c8c082b14074a6e84da694ef2f35","webhook_id":"dd11c41f93a1ec05751fb1b2008d247fc3895548b22b21ddb7d013a02475264b"},"disabled_by":null,"discovery_keys":{},"domain":"mobile_app","entry_id":"01JT3VZ248SG5C78NYX10R3VYC","minor_version":1,"modified_at":"2025-04-30T17:44:59.821405+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"registration","subentries":[],"title":"Factorys iPad","unique_id":"io.robbie.HomeAssistant-98A9293F-CB61-45EC-B9B6-192CF169CDAD","version":1}
"id": "Cloud Key Gen2 Plus"
},
"disabled_by": null,
"domain": "unifiprotect",
"entry_id": "55db3b46f3bf75777e4779fd25ed6bca",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"disable_rtsp": false,
"all_updates": false,
"override_connection_host": false,
"max_media": 1000,
"allow_ea": false
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Cloud Key Gen2 Plus",
"unique_id": "70A741A53E33",
"version": 2
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"type": "Setup as remote node"
},
"disabled_by": null,
"domain": "remote_homeassistant",
"entry_id": "475e896c4033e0014ade6d0ef18b8329",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Remote instance",
"unique_id": "remote",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"url": "http://172.22.114.136:9197/dmr",
"device_id": "uuid:c73ad4db-6241-496a-80fd-994a43d1c980",
"type": "urn:schemas-upnp-org:device:MediaRenderer:1",
"mac": "d0:03:df:ca:7c:74"
},
"disabled_by": null,
"domain": "dlna_dmr",
"entry_id": "d24486392bce9e68c96bb73e691681e4",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "[TV] Samsung TV 1048 Right",
"unique_id": "uuid:c73ad4db-6241-496a-80fd-994a43d1c980",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"url": "http://172.22.114.134:9197/dmr",
"device_id": "uuid:5cac41f8-5fa8-4d42-9c30-8c8b3f6050b0",
"type": "urn:schemas-upnp-org:device:MediaRenderer:1",
"mac": "24:fc:e5:5a:ff:68"
},
"disabled_by": null,
"domain": "dlna_dmr",
"entry_id": "e172a44f6bd05f602b6469920fdc3754",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "[TV] Samsung TV 1048 Left",
"unique_id": "uuid:5cac41f8-5fa8-4d42-9c30-8c8b3f6050b0",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"url": "http://172.22.114.135:9197/dmr",
"device_id": "uuid:8e7e5e53-46a1-4974-9513-05270ad0af77",
"type": "urn:schemas-upnp-org:device:MediaRenderer:1",
"mac": "24:fc:e5:5b:17:6a"
},
"disabled_by": null,
"domain": "dlna_dmr",
"entry_id": "b02ec81df7205825511646b73cbb58f8",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "[TV] Samsung TV 1048 Center",
"unique_id": "uuid:8e7e5e53-46a1-4974-9513-05270ad0af77",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "172.22.114.135",
"mac": "24:fc:e5:5b:17:6a",
"model": "UN65RU8000FXZA",
"ssdp_rendering_control_location": "http://172.22.114.135:9197/dmr"
},
"disabled_by": null,
"domain": "samsungtv",
"entry_id": "6e73159a0788a6dbb3ccf4dc7a26945d",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "Samsung TV 1048 Center (UN65RU8000FXZA)",
"unique_id": "89947ca0-b33a-4912-b659-f4b1578a7777",
"version": 2
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "172.22.114.134",
"mac": "24:fc:e5:5a:ff:68",
"model": "UN65RU8000FXZA",
"ssdp_rendering_control_location": "http://172.22.114.134:9197/dmr"
},
"disabled_by": null,
"domain": "samsungtv",
"entry_id": "5c87c2ce60943c4288dfd80aa9db5bf0",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "Samsung TV 1048 Left (UN65RU8000FXZA)",
"unique_id": "64bfe2c2-7b01-46b8-89d3-88c49373f55e",
"version": 2
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "remote_homeassistant",
"entry_id": "0200f22cdff3cef1905acfee956d71cc",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "Remote: Innovation Factory",
"unique_id": "311fbea761c143be9c14dc1cd5ab57b2",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "sun",
"entry_id": "0feafcef6c9ee4eb380cad7190b2f403",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "import",
"title": "Sun",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "octoprint",
"entry_id": "7a20eaee207e2873f5296d198ac8dd63",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "OctoPrint Printer: 172.22.114.150",
"unique_id": "aef76126-1c40-459c-a460-ca7749222fdd",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"app_id": "io.homeassistant.companion.android",
"app_name": "Home Assistant",
"app_version": "2024.4.1-full (12576)",
"device_name": "lab-phone",
"manufacturer": "samsung",
"model": "SM-A546U1",
"os_name": "Android",
"os_version": "34",
"supports_encryption": false,
"app_data": {
"push_websocket_channel": true,
"push_url": "https://mobile-apps.home-assistant.io/api/sendPush/android/v1",
"push_token": "filDMYq4Sa2akEe_BlkVyg:APA91bHPxz2u6XjWrUIErPeakuxA-_VTCZT5JVa0vD5gkpe0P65aZhDC0Q6uNRQWJ2nQ3a6xRjI7uaB0ywup_WwSQNd8AkTqQlObEPr0AWO6ditliwFuh0EQWGQzU1rYFKiz7gq8zhBW"
},
"device_id": "2ddef885064fb8ed",
"webhook_id": "541b6ac0d1f592acf1d6dc8ff736c2bad653ee1e85a8b6409348c1eb974c6ce7",
"user_id": "5ef2c8c082b14074a6e84da694ef2f35"
},
"disabled_by": null,
"domain": "mobile_app",
"entry_id": "e3427a6f1a531d4647c57351962f3e1a",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "registration",
"title": "lab-phone",
"unique_id": "io.homeassistant.companion.android-2ddef885064fb8ed",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "172.22.113.18",
"mac": "70:09:71:0d:f6:5d",
"model": "UN50AU8000FXZA",
"ssdp_rendering_control_location": "http://172.22.113.18:9197/dmr"
},
"disabled_by": null,
"domain": "samsungtv",
"entry_id": "54d38b0b04614f1d163c9ba14ce22fed",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"title": "Samsung AU8000 50 TV (UN50AU8000FXZA)",
"unique_id": "7671ae54-70ac-426b-8fa0-a065cd4bd428",
"version": 2
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "mjpeg",
"entry_id": "f92d6ca163501ea7659047c60ba4e9e5",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"authentication": "basic",
"mjpeg_url": "http://meyer:8080",
"password": "",
"still_image_url": null,
"username": null,
"verify_ssl": false
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Crack Output",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "192.168.1.151",
"sleep_period": 0,
"model": "SNSW-001X15UL",
"gen": 2,
"port": 80
},
"disabled_by": null,
"domain": "shelly",
"entry_id": "04978edcf23c54a047e4f421779754ad",
"minor_version": 1,
"modified_at": "2024-09-30T14:21:49.532296+00:00",
"options": {
"ble_scanner_mode": "passive"
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Sheller Drum Enable",
"unique_id": "CC7B5C0D0EB4",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "192.168.1.15",
"sleep_period": 0,
"model": "SNSW-001X15UL",
"gen": 2,
"port": 80
},
"disabled_by": null,
"domain": "shelly",
"entry_id": "ce337fdb50b165d7ba080505d5c73343",
"minor_version": 1,
"modified_at": "2024-09-30T14:21:49.516809+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Sheller Paddle Shaft Enable",
"unique_id": "CC7B5C0D316C",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "192.168.1.222",
"sleep_period": 0,
"model": "SNDM-00100WW",
"gen": 2,
"port": 80
},
"disabled_by": null,
"domain": "shelly",
"entry_id": "779bd6f1f6eebd9fb67b45fa40386e0c",
"minor_version": 1,
"modified_at": "2024-09-30T14:21:49.537818+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Sheller Drum Velocity",
"unique_id": "E86BEAE4D350",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "192.168.1.28",
"sleep_period": 0,
"model": "SNDM-00100WW",
"gen": 2
},
"disabled_by": null,
"domain": "shelly",
"entry_id": "51355cd442e2d0c51a3a43811555ee77",
"minor_version": 1,
"modified_at": "2024-09-30T14:21:49.528183+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Sheller Paddle Shaft Velocity",
"unique_id": "E86BEAE4DF24",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "192.168.1.92",
"alias": "TP-LINK_Power Strip_D7C1",
"model": "HS300(US)",
"connection_parameters": {
"device_family": "IOT.SMARTPLUGSWITCH",
"encryption_type": "XOR"
},
"uses_http": false
},
"disabled_by": null,
"domain": "tplink",
"entry_id": "37a922a368171d96e691a3439549d7bf",
"minor_version": 5,
"modified_at": "2024-09-30T14:21:49.546287+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "TP-LINK_Power Strip_D7C1 HS300(US)",
"unique_id": "98:25:4a:f7:d7:c1",
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "switch_as_x",
"entry_id": "e49f29f5d0e10a3ef63369b4c8c7f5c2",
"minor_version": 2,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"entity_id": "switch.tp_link_power_strip_d7c1_light_2",
"invert": false,
"target_domain": "light"
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Light",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "switch_as_x",
"entry_id": "77ef41d0bbf25d6007b4b2968dc60f58",
"minor_version": 2,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"entity_id": "switch.tp_link_power_strip_d7c1_light_3",
"invert": false,
"target_domain": "light"
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Light",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "switch_as_x",
"entry_id": "54af0f5338887810d55839a908975bd3",
"minor_version": 2,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"entity_id": "switch.tp_link_power_strip_d7c1_light_4",
"invert": false,
"target_domain": "light"
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Light ",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "group",
"entry_id": "7451d239431f2b6ea2ee2a1b85ab5c56",
"minor_version": 1,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"group_type": "light",
"name": "Conveyor Lights",
"entities": [
"light.tp_link_power_strip_d7c1_light",
"light.tp_link_power_strip_d7c1_light_2",
"light.tp_link_power_strip_d7c1_light_3",
"light.tp_link_power_strip_d7c1_light_4"
],
"hide_members": false,
"all": false
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Conveyor Lights",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "switch_as_x",
"entry_id": "82a8d9eef25b35d19d72cd6ce5f9dcbe",
"minor_version": 2,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"entity_id": "switch.tp_link_power_strip_d7c1_light",
"invert": false,
"target_domain": "light"
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Light ",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {},
"disabled_by": null,
"domain": "switch_as_x",
"entry_id": "f1e37a42d2b569eaa4ac24c20a31fa24",
"minor_version": 2,
"modified_at": "1970-01-01T00:00:00+00:00",
"options": {
"entity_id": "switch.tp_link_power_strip_d7c1_zima_board",
"invert": false,
"target_domain": "light"
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "Vibratory Lights",
"unique_id": null,
"version": 1
},
{
"created_at": "1970-01-01T00:00:00+00:00",
"data": {
"host": "192.168.1.139",
"port": 80,
"sleep_period": 0,
"model": "SNSW-001P15UL",
"gen": 2
},
"disabled_by": null,
"domain": "shelly",
"entry_id": "01J5NH0A41TYMRRGASE2QJ0SQ5",
"minor_version": 2,
"modified_at": "2024-09-30T14:21:49.522251+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"title": "shellyplus1pm-c049ef8c7310",
"unique_id": "C049EF8C7310",
"version": 1
},
{
"created_at": "2024-09-30T14:21:49.515314+00:00",
"data": {},
"disabled_by": null,
"domain": "hassio",
"entry_id": "01J91MY5JBJH64GQS4WA5SYVK2",
"minor_version": 1,
"modified_at": "2024-09-30T14:21:49.515328+00:00",
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "system",
"title": "Supervisor",
"unique_id": "hassio",
"version": 1
}
] ]
} }
} }

View File

@ -1,46 +1,89 @@
{ {
"version": 1, "version": 1,
"minor_version": 8, "minor_version": 9,
"key": "core.device_registry", "key": "core.device_registry",
"data": { "data": {
"devices": [ "devices": [
{"area_id":null,"config_entries":["c0c49687c85dee58eea3dbb414dd9337"],"configuration_url":null,"connections":[["mac","60:01:94:cf:79:15"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"130c399e6b93586793dfd365ea5b3ebc","identifiers":[],"labels":[],"manufacturer":"Espressif","model":"esp01_1m","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"jc-vibratory","primary_config_entry":"c0c49687c85dee58eea3dbb414dd9337","serial_number":null,"sw_version":"2023.7.1 (Jan 18 2024, 19:34:19)","via_device_id":null}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/65ba9b25013a7903e400256f","connections":[["mac","e4:38:83:0f:d1:b8"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"a00c8648172b02bb56e52bd1083bc8b5","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G3 Flex","model_id":"UVC G3 Flex","modified_at":"2025-04-24T19:41:34.413368+00:00","name_by_user":null,"name":"G3 Flex 01","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.43","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176/protect/devices/65ba9b25013a7903e400256f","connections":[["mac","e4:38:83:0f:d1:b8"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"a00c8648172b02bb56e52bd1083bc8b5","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"UVC G3 Flex","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"G3 Flex 01","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.71.147","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176","connections":[["mac","70:a7:41:a5:3e:33"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"437dbead96a87ac711c7a4d99a79c6cb","identifiers":[["unifiprotect","70A741A53E33"]],"labels":[],"manufacturer":"Ubiquiti","model":"UCK-G2-PLUS","model_id":null,"modified_at":"2025-04-30T15:19:53.895791+00:00","name_by_user":null,"name":"Cloud Key Gen2 Plus","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"5.3.45","via_device_id":null},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176","connections":[["mac","70:a7:41:a5:3e:33"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"437dbead96a87ac711c7a4d99a79c6cb","identifiers":[["unifiprotect","70A741A53E33"]],"labels":[],"manufacturer":"Ubiquiti","model":"UCK-G2-PLUS","model_id":null,"modified_at":"2024-09-30T14:21:39.083371+00:00","name_by_user":null,"name":"Cloud Key Gen2 Plus","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"5.0.34","via_device_id":null}, {"area_id":null,"config_entries":["0feafcef6c9ee4eb380cad7190b2f403"],"config_entries_subentries":{"0feafcef6c9ee4eb380cad7190b2f403":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"837a78425d2847cdcbe0b38f7d05cb7b","identifiers":[["sun","0feafcef6c9ee4eb380cad7190b2f403"]],"labels":[],"manufacturer":null,"model":null,"model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Sun","primary_config_entry":"0feafcef6c9ee4eb380cad7190b2f403","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["0feafcef6c9ee4eb380cad7190b2f403"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"837a78425d2847cdcbe0b38f7d05cb7b","identifiers":[["sun","0feafcef6c9ee4eb380cad7190b2f403"]],"labels":[],"manufacturer":null,"model":null,"model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Sun","primary_config_entry":"0feafcef6c9ee4eb380cad7190b2f403","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"d96ab4ca2878dfd648ac7c530f4d0268","identifiers":[["mqtt","steinlite-moisture-meter"]],"labels":[],"manufacturer":"Steinlite","model":"SB900","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Moisture Meter","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"d96ab4ca2878dfd648ac7c530f4d0268","identifiers":[["mqtt","steinlite-moisture-meter"]],"labels":[],"manufacturer":"Steinlite","model":"SB900","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Moisture Meter","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"682585d3e49461db68dfce310711e240","identifiers":[["mqtt","sheller-scale"]],"labels":[],"manufacturer":"Adam Equipment","model":"CPWplus 15","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Sheller Scale","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"682585d3e49461db68dfce310711e240","identifiers":[["mqtt","sheller-scale"]],"labels":[],"manufacturer":"Adam Equipment","model":"CPWplus 15","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Sheller Scale","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"704baf0c713921ab159af1306f741d87","identifiers":[["mqtt","precision-scale"]],"labels":[],"manufacturer":"U.S. Solid","model":"USS-DBS87-310G","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Precision Scale","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"704baf0c713921ab159af1306f741d87","identifiers":[["mqtt","precision-scale"]],"labels":[],"manufacturer":"U.S. Solid","model":"USS-DBS87-310G","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Precision Scale","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["e3427a6f1a531d4647c57351962f3e1a"],"config_entries_subentries":{"e3427a6f1a531d4647c57351962f3e1a":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"9b53abd12d274cfc0a304b0b15ebbd39","identifiers":[["mobile_app","2ddef885064fb8ed"]],"labels":[],"manufacturer":"samsung","model":"SM-A546U1","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"lab-phone","primary_config_entry":"e3427a6f1a531d4647c57351962f3e1a","serial_number":null,"sw_version":"34","via_device_id":null},
{"area_id":null,"config_entries":["e3427a6f1a531d4647c57351962f3e1a"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"9b53abd12d274cfc0a304b0b15ebbd39","identifiers":[["mobile_app","2ddef885064fb8ed"]],"labels":[],"manufacturer":"samsung","model":"SM-A546U1","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"lab-phone","primary_config_entry":"e3427a6f1a531d4647c57351962f3e1a","serial_number":null,"sw_version":"34","via_device_id":null}, {"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"feec748cc156a1ca441d38caf620ecfe","identifiers":[["mqtt","moisture-station-scanner"]],"labels":[],"manufacturer":"Netum","model":"C300-HF","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"RFID Tag Scanner","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"feec748cc156a1ca441d38caf620ecfe","identifiers":[["mqtt","moisture-station-scanner"]],"labels":[],"manufacturer":"Netum","model":"C300-HF","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"RFID Tag Scanner","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"c9fd6d12a759980d491da079f9e3a545","identifiers":[["mqtt","scale-station-scanner"]],"labels":[],"manufacturer":"Sony","model":"RC-S380","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"RFID Tag Scanner","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"c9fd6d12a759980d491da079f9e3a545","identifiers":[["mqtt","scale-station-scanner"]],"labels":[],"manufacturer":"Sony","model":"RC-S380","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"RFID Tag Scanner","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["f92d6ca163501ea7659047c60ba4e9e5"],"config_entries_subentries":{"f92d6ca163501ea7659047c60ba4e9e5":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"387080b8991325a3e49c3b3e94736b27","identifiers":[["mjpeg","f92d6ca163501ea7659047c60ba4e9e5"]],"labels":[],"manufacturer":null,"model":null,"model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Crack Output","primary_config_entry":"f92d6ca163501ea7659047c60ba4e9e5","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["f92d6ca163501ea7659047c60ba4e9e5"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"387080b8991325a3e49c3b3e94736b27","identifiers":[["mjpeg","f92d6ca163501ea7659047c60ba4e9e5"]],"labels":[],"manufacturer":null,"model":null,"model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Crack Output","primary_config_entry":"f92d6ca163501ea7659047c60ba4e9e5","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["04978edcf23c54a047e4f421779754ad"],"config_entries_subentries":{"04978edcf23c54a047e4f421779754ad":[null]},"configuration_url":"http://192.168.1.152:80","connections":[["mac","cc:7b:5c:0d:0e:b4"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"98bc66339afcb5b0ee18bd93df9ab0f0","identifiers":[["shelly","CC7B5C0D0EB4"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1 UL","model_id":"SNSW-001X15UL","modified_at":"2025-04-08T16:18:55.552123+00:00","name_by_user":"sheller-drum-enable","name":"shellyplus1-cc7b5c0d0eb4","primary_config_entry":"04978edcf23c54a047e4f421779754ad","serial_number":null,"sw_version":"20250318-152131/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["04978edcf23c54a047e4f421779754ad"],"configuration_url":"http://192.168.1.151:80","connections":[["mac","cc:7b:5c:0d:0e:b4"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"98bc66339afcb5b0ee18bd93df9ab0f0","identifiers":[],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1 UL","model_id":"SNSW-001X15UL","modified_at":"2024-09-30T14:21:49.997137+00:00","name_by_user":"sheller-drum-enable","name":"shellyplus1-cc7b5c0d0eb4","primary_config_entry":"04978edcf23c54a047e4f421779754ad","serial_number":null,"sw_version":"20240819-074322/1.4.2-gc2639da","via_device_id":null}, {"area_id":null,"config_entries":["ce337fdb50b165d7ba080505d5c73343"],"config_entries_subentries":{"ce337fdb50b165d7ba080505d5c73343":[null]},"configuration_url":"http://192.168.1.15:80","connections":[["mac","cc:7b:5c:0d:31:6c"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"74cb8e9e52e8403f6627e79e75994c31","identifiers":[["shelly","CC7B5C0D316C"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1 UL","model_id":"SNSW-001X15UL","modified_at":"2025-03-28T16:50:09.870954+00:00","name_by_user":"sheller-paddle-shaft-enable","name":"shellyplus1-cc7b5c0d316c","primary_config_entry":"ce337fdb50b165d7ba080505d5c73343","serial_number":null,"sw_version":"20250318-152131/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["ce337fdb50b165d7ba080505d5c73343"],"configuration_url":"http://192.168.1.15:80","connections":[["mac","cc:7b:5c:0d:31:6c"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"74cb8e9e52e8403f6627e79e75994c31","identifiers":[],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1 UL","model_id":"SNSW-001X15UL","modified_at":"2024-09-30T14:21:49.990213+00:00","name_by_user":"sheller-paddle-shaft-enable","name":"shellyplus1-cc7b5c0d316c","primary_config_entry":"ce337fdb50b165d7ba080505d5c73343","serial_number":null,"sw_version":"20240819-074322/1.4.2-gc2639da","via_device_id":null}, {"area_id":null,"config_entries":["779bd6f1f6eebd9fb67b45fa40386e0c"],"config_entries_subentries":{"779bd6f1f6eebd9fb67b45fa40386e0c":[null]},"configuration_url":"http://192.168.1.222:80","connections":[["mac","e8:6b:ea:e4:d3:50"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"9e9ad673334459b49715731f2df83ff4","identifiers":[["shelly","E86BEAE4D350"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 0-10V Dimmer","model_id":"SNDM-00100WW","modified_at":"2025-03-28T16:50:10.101529+00:00","name_by_user":"sheller-drum-velocity","name":"shellyplus010v-e86beae4d350","primary_config_entry":"779bd6f1f6eebd9fb67b45fa40386e0c","serial_number":null,"sw_version":"20250318-152134/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["779bd6f1f6eebd9fb67b45fa40386e0c"],"configuration_url":"http://192.168.1.222:80","connections":[["mac","e8:6b:ea:e4:d3:50"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"9e9ad673334459b49715731f2df83ff4","identifiers":[],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 0-10V Dimmer","model_id":"SNDM-00100WW","modified_at":"2024-09-30T14:21:50.092903+00:00","name_by_user":"sheller-drum-velocity","name":"shellyplus010v-e86beae4d350","primary_config_entry":"779bd6f1f6eebd9fb67b45fa40386e0c","serial_number":null,"sw_version":"20240819-074324/1.4.2-gc2639da","via_device_id":null}, {"area_id":null,"config_entries":["51355cd442e2d0c51a3a43811555ee77"],"config_entries_subentries":{"51355cd442e2d0c51a3a43811555ee77":[null]},"configuration_url":"http://192.168.1.28:80","connections":[["mac","e8:6b:ea:e4:df:24"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"78045cb98b008cd63f1d086a45326c7f","identifiers":[["shelly","E86BEAE4DF24"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 0-10V Dimmer","model_id":"SNDM-00100WW","modified_at":"2025-02-12T17:45:59.363846+00:00","name_by_user":"sheller-paddle-shaft-velocity","name":"shellyplus010v-e86beae4df24","primary_config_entry":"51355cd442e2d0c51a3a43811555ee77","serial_number":null,"sw_version":"20241011-114443/1.4.4-g6d2a586","via_device_id":null},
{"area_id":null,"config_entries":["51355cd442e2d0c51a3a43811555ee77"],"configuration_url":"http://192.168.1.28:80","connections":[["mac","e8:6b:ea:e4:df:24"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"78045cb98b008cd63f1d086a45326c7f","identifiers":[],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 0-10V Dimmer","model_id":"SNDM-00100WW","modified_at":"2024-09-30T14:21:50.449990+00:00","name_by_user":"sheller-paddle-shaft-velocity","name":"shellyplus010v-e86beae4df24","primary_config_entry":"51355cd442e2d0c51a3a43811555ee77","serial_number":null,"sw_version":"20240819-074324/1.4.2-gc2639da","via_device_id":null}, {"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"4a2ee843944a1c105a8a2ac44be60e23","identifiers":[["mqtt","shelling-machine"]],"labels":[],"manufacturer":"ME&E","model":null,"model_id":null,"modified_at":"2024-11-19T21:54:25.594657+00:00","name_by_user":null,"name":"Shelling Machine","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"4a2ee843944a1c105a8a2ac44be60e23","identifiers":[["mqtt","shelling-machine"]],"labels":[],"manufacturer":"ME&E","model":null,"model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"Shelling Machine","primary_config_entry":null,"serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf","f1e37a42d2b569eaa4ac24c20a31fa24","82a8d9eef25b35d19d72cd6ce5f9dcbe","54af0f5338887810d55839a908975bd3","77ef41d0bbf25d6007b4b2968dc60f58","e49f29f5d0e10a3ef63369b4c8c7f5c2"],"config_entries_subentries":{"77ef41d0bbf25d6007b4b2968dc60f58":[null],"f1e37a42d2b569eaa4ac24c20a31fa24":[null],"54af0f5338887810d55839a908975bd3":[null],"37a922a368171d96e691a3439549d7bf":[null],"e49f29f5d0e10a3ef63369b4c8c7f5c2":[null],"82a8d9eef25b35d19d72cd6ce5f9dcbe":[null]},"configuration_url":null,"connections":[["mac","98:25:4a:f7:d7:c1"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"8bb02a4d9bcee08c25823bd8a2ee88f3","identifiers":[["tplink","98:25:4A:F7:D7:C1"]],"labels":[],"manufacturer":"TP-Link","model":"HS300","model_id":null,"modified_at":"2025-03-08T19:42:43.429891+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":null},
{"area_id":null,"config_entries":["e49f29f5d0e10a3ef63369b4c8c7f5c2","77ef41d0bbf25d6007b4b2968dc60f58","82a8d9eef25b35d19d72cd6ce5f9dcbe","f1e37a42d2b569eaa4ac24c20a31fa24","37a922a368171d96e691a3439549d7bf","54af0f5338887810d55839a908975bd3"],"configuration_url":null,"connections":[["mac","98:25:4a:f7:d7:c1"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"8bb02a4d9bcee08c25823bd8a2ee88f3","identifiers":[["tplink","98:25:4A:F7:D7:C1"]],"labels":[],"manufacturer":"TP-Link","model":"HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":null}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/668da48800dbe603e4002cdf","connections":[["mac","e4:38:83:0f:d1:97"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"cb1143714d798e2e0e5e4b38476c2729","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G3 Flex","model_id":"UVC G3 Flex","modified_at":"2025-04-28T15:39:46.230184+00:00","name_by_user":null,"name":"G3 Flex 02","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176/protect/devices/668da48800dbe603e4002cdf","connections":[["mac","e4:38:83:0f:d1:97"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"cb1143714d798e2e0e5e4b38476c2729","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"UVC G3 Flex","model_id":null,"modified_at":"2024-09-30T14:21:38.970906+00:00","name_by_user":null,"name":"G3 Flex 02","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.72.36","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/668daa1e019ce603e4002d31","connections":[["mac","f4:e2:c6:70:d6:da"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"7bc725f99e3f2e1461c8584f6e852853","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"AI 360","model_id":"UVC AI 360","modified_at":"2025-04-28T15:39:46.228701+00:00","name_by_user":null,"name":"AI 360","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176/protect/devices/668daa1e019ce603e4002d31","connections":[["mac","f4:e2:c6:70:d6:da"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"7bc725f99e3f2e1461c8584f6e852853","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"UVC AI 360","model_id":null,"modified_at":"2024-09-30T14:21:38.949223+00:00","name_by_user":null,"name":"AI 360","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.72.36","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/66954d8403c61e03e4001efe","connections":[["mac","e4:38:83:0f:d1:df"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"01e8126721b48af081f5fc194eb779e0","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G3 Flex","model_id":"UVC G3 Flex","modified_at":"2025-04-28T15:39:46.229996+00:00","name_by_user":null,"name":"G3 Flex 03","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176/protect/devices/66954d8403c61e03e4001efe","connections":[["mac","e4:38:83:0f:d1:df"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"01e8126721b48af081f5fc194eb779e0","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"UVC G3 Flex","model_id":null,"modified_at":"2024-09-30T14:21:38.942776+00:00","name_by_user":null,"name":"G3 Flex 03","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.72.36","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/66ab86e30161d503e4001c16","connections":[["mac","ac:8b:a9:9f:a1:d2"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"6148e0856ead1b5ad475a55467c93042","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.230587+00:00","name_by_user":null,"name":"G5 Flex 01","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176/protect/devices/66ab86e30161d503e4001c16","connections":[["mac","ac:8b:a9:9f:a1:d2"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"6148e0856ead1b5ad475a55467c93042","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"UVC G5 Flex","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"G5 Flex 01","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.71.149","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"}, {"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/66ab87e7017dd503e4001c60","connections":[["mac","e4:38:83:0c:f4:ab"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"de40a755e95c6edd9cc9c549a43f3796","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.229782+00:00","name_by_user":null,"name":"G5 Flex 02","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"configuration_url":"https://172.22.114.176/protect/devices/66ab87e7017dd503e4001c60","connections":[["mac","e4:38:83:0c:f4:ab"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"de40a755e95c6edd9cc9c549a43f3796","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"UVC G5 Flex","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"G5 Flex 02","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.71.149","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"config_entries_subentries":{"37a922a368171d96e691a3439549d7bf":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"0a6798a5f44c2a8dd6cc493bf4373ebb","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC00"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Zima Board","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"},
{"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"0a6798a5f44c2a8dd6cc493bf4373ebb","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC00"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Zima Board","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"config_entries_subentries":{"37a922a368171d96e691a3439549d7bf":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"0c46694b6553f98c5c5d36dee090abcd","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC01"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light ","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"},
{"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"0c46694b6553f98c5c5d36dee090abcd","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC01"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light ","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"config_entries_subentries":{"37a922a368171d96e691a3439549d7bf":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"2580b7a409e3edbd9fe0594649642454","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC02"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"},
{"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"2580b7a409e3edbd9fe0594649642454","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC02"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"config_entries_subentries":{"37a922a368171d96e691a3439549d7bf":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"12984d61269adcddd72e9e302db1a4fa","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC03"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"},
{"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"12984d61269adcddd72e9e302db1a4fa","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC03"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"config_entries_subentries":{"37a922a368171d96e691a3439549d7bf":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"a118052d327d7ab956d83b1e50a7addf","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC04"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light ","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"},
{"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"a118052d327d7ab956d83b1e50a7addf","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC04"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Light ","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"}, {"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"config_entries_subentries":{"37a922a368171d96e691a3439549d7bf":[null]},"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"24c8694e03be3bb519577b12f4ca13a5","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC05"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Vibratory Conveyor","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"},
{"area_id":null,"config_entries":["37a922a368171d96e691a3439549d7bf"],"configuration_url":null,"connections":[],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"2.0","id":"24c8694e03be3bb519577b12f4ca13a5","identifiers":[["tplink","98:25:4A:F7:D7:C1_8006AD27BB0C6D6EB0092A56145EE0B8224A9BFC05"]],"labels":[],"manufacturer":"TP-Link","model":"Socket for HS300(US)","model_id":null,"modified_at":"1970-01-01T00:00:00+00:00","name_by_user":null,"name":"TP-LINK_Power Strip_D7C1 Vibratory Conveyor","primary_config_entry":"37a922a368171d96e691a3439549d7bf","serial_number":null,"sw_version":"1.0.12 Build 220121 Rel.175814","via_device_id":"8bb02a4d9bcee08c25823bd8a2ee88f3"}, {"area_id":"jc_machine","config_entries":["01J5NH0A41TYMRRGASE2QJ0SQ5"],"config_entries_subentries":{"01J5NH0A41TYMRRGASE2QJ0SQ5":[null]},"configuration_url":"http://192.168.1.139:80","connections":[["mac","c0:49:ef:8c:73:10"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"bdc5f90b963bab0edf03bfeeff494858","identifiers":[["shelly","C049EF8C7310"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1PM UL","model_id":"SNSW-001P15UL","modified_at":"2025-04-14T17:18:52.934204+00:00","name_by_user":"JC Vibratory Feed Conveyor","name":"shellyplus1pm-c049ef8c7310","primary_config_entry":"01J5NH0A41TYMRRGASE2QJ0SQ5","serial_number":null,"sw_version":"20250318-152121/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["01J5NH0A41TYMRRGASE2QJ0SQ5"],"configuration_url":"http://192.168.1.139:80","connections":[["mac","c0:49:ef:8c:73:10"]],"created_at":"1970-01-01T00:00:00+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"bdc5f90b963bab0edf03bfeeff494858","identifiers":[],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1PM UL","model_id":"SNSW-001P15UL","modified_at":"2024-09-30T14:21:50.007317+00:00","name_by_user":"JC Vibratory Feed Conveyor","name":"shellyplus1pm-c049ef8c7310","primary_config_entry":"01J5NH0A41TYMRRGASE2QJ0SQ5","serial_number":null,"sw_version":"20240819-074343/1.4.2-gc2639da","via_device_id":null}, {"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":"homeassistant://hassio/addon/a0d7b954_emqx","connections":[],"created_at":"2024-09-30T14:21:50.117815+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"361a4984b7d01d3080714018482ccc7c","identifiers":[["hassio","a0d7b954_emqx"]],"labels":[],"manufacturer":"Home Assistant Community Add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2025-04-03T12:49:10.971877+00:00","name_by_user":null,"name":"EMQX","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"0.7.5","via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":"homeassistant://hassio/addon/a0d7b954_emqx","connections":[],"created_at":"2024-09-30T14:21:50.117815+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"361a4984b7d01d3080714018482ccc7c","identifiers":[["hassio","a0d7b954_emqx"]],"labels":[],"manufacturer":"Home Assistant Community Add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2024-09-30T14:21:50.118023+00:00","name_by_user":null,"name":"EMQX","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"0.7.0","via_device_id":null}, {"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":"homeassistant://hassio/addon/a0d7b954_ssh","connections":[],"created_at":"2024-09-30T14:21:50.130433+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"9eb0166d98958fe13f7a50eb706ffc4c","identifiers":[["hassio","a0d7b954_ssh"]],"labels":[],"manufacturer":"Home Assistant Community Add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2025-02-17T17:27:42.631137+00:00","name_by_user":null,"name":"Advanced SSH & Web Terminal","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"20.0.0","via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":"homeassistant://hassio/addon/a0d7b954_vscode","connections":[],"created_at":"2024-09-30T14:21:50.119745+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"4337d0ff05fb782c9ec2ae035488a00a","identifiers":[["hassio","a0d7b954_vscode"]],"labels":[],"manufacturer":"Home Assistant Community Add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2024-09-30T21:51:30.243506+00:00","name_by_user":null,"name":"Studio Code Server","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"5.16.1","via_device_id":null}, {"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.133480+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"e8c8f69e8318af2be25fae388c7d3e9c","identifiers":[["hassio","core"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Core","model_id":null,"modified_at":"2025-04-28T15:39:43.376367+00:00","name_by_user":null,"name":"Home Assistant Core","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"2025.4.4","via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":"homeassistant://hassio/addon/a0d7b954_ssh","connections":[],"created_at":"2024-09-30T14:21:50.130433+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"9eb0166d98958fe13f7a50eb706ffc4c","identifiers":[["hassio","a0d7b954_ssh"]],"labels":[],"manufacturer":"Home Assistant Community Add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2024-09-30T14:21:50.130703+00:00","name_by_user":null,"name":"Advanced SSH & Web Terminal","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"19.0.0","via_device_id":null}, {"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.142602+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"adffec60f63f0032597d03eba833062f","identifiers":[["hassio","supervisor"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Supervisor","model_id":null,"modified_at":"2025-04-28T15:39:43.376487+00:00","name_by_user":null,"name":"Home Assistant Supervisor","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"2025.04.1","via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.133480+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"e8c8f69e8318af2be25fae388c7d3e9c","identifiers":[["hassio","core"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Core","model_id":null,"modified_at":"2024-09-30T14:21:50.133962+00:00","name_by_user":null,"name":"Home Assistant Core","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"2024.9.3","via_device_id":null}, {"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.143809+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"c0da2b39c57cd1e833c93a1e0d6081a3","identifiers":[["hassio","host"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Host","model_id":null,"modified_at":"2024-09-30T14:21:50.143950+00:00","name_by_user":null,"name":"Home Assistant Host","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.142602+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"adffec60f63f0032597d03eba833062f","identifiers":[["hassio","supervisor"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Supervisor","model_id":null,"modified_at":"2024-09-30T14:21:50.142707+00:00","name_by_user":null,"name":"Home Assistant Supervisor","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"2024.09.1","via_device_id":null}, {"area_id":null,"config_entries":["01JAT8AZHNSF6WPATHNE5D6XM4"],"config_entries_subentries":{"01JAT8AZHNSF6WPATHNE5D6XM4":[null]},"configuration_url":null,"connections":[],"created_at":"2024-10-22T14:25:11.025424+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"4733401a628a81b68b232c34f905301b","identifiers":[["browser_mod","pecan-station"]],"labels":[],"manufacturer":"Browser Mod","model":null,"model_id":null,"modified_at":"2024-10-22T14:25:11.025531+00:00","name_by_user":null,"name":"pecan-station","primary_config_entry":"01JAT8AZHNSF6WPATHNE5D6XM4","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.143809+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"c0da2b39c57cd1e833c93a1e0d6081a3","identifiers":[["hassio","host"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Host","model_id":null,"modified_at":"2024-09-30T14:21:50.143950+00:00","name_by_user":null,"name":"Home Assistant Host","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":null,"via_device_id":null}, {"area_id":null,"config_entries":["01JC1CF88K7YTRGZT79CYQS536"],"config_entries_subentries":{"01JC1CF88K7YTRGZT79CYQS536":[null]},"configuration_url":null,"connections":[["mac","54:af:97:09:93:f8"]],"created_at":"2024-11-06T18:40:57.705358+00:00","disabled_by":null,"entry_type":null,"hw_version":"1.0","id":"15c657490c8db92b1bc833658f6569b6","identifiers":[["tplink","54:AF:97:09:93:F8"]],"labels":[],"manufacturer":"TP-Link","model":"KP115","model_id":null,"modified_at":"2025-01-07T17:42:11.829574+00:00","name_by_user":null,"name":"Vibratory Conveyor","primary_config_entry":"01JC1CF88K7YTRGZT79CYQS536","serial_number":null,"sw_version":"1.0.20 Build 221125 Rel.092759","via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.145121+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"db658cf1b7110495bd574ecbfea00f65","identifiers":[["hassio","OS"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Operating System","model_id":null,"modified_at":"2024-09-30T14:21:50.145227+00:00","name_by_user":null,"name":"Home Assistant Operating System","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"13.1","via_device_id":null} {"area_id":null,"config_entries":["01JCEH26PKQ5WZ0GQ495DCG9WM"],"config_entries_subentries":{"01JCEH26PKQ5WZ0GQ495DCG9WM":[null]},"configuration_url":"http://192.168.1.75:80","connections":[["mac","b8:d6:1a:87:d2:a8"]],"created_at":"2024-11-11T21:13:55.808375+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"084130f8681211cd468bd18045eb1ab9","identifiers":[["shelly","B8D61A87D2A8"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1 UL","model_id":"SNSW-001X15UL","modified_at":"2025-04-08T16:18:28.942959+00:00","name_by_user":null,"name":"shellyplus1-b8d61a87d2a8","primary_config_entry":"01JCEH26PKQ5WZ0GQ495DCG9WM","serial_number":null,"sw_version":"20250318-152131/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["01JCEH2A2K8Y9VEK9XV8ZPC69H"],"config_entries_subentries":{"01JCEH2A2K8Y9VEK9XV8ZPC69H":[null]},"configuration_url":"http://192.168.1.7:80","connections":[["mac","b8:d6:1a:8a:75:08"]],"created_at":"2024-11-11T21:13:58.504970+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"fde74ce0fd1f87791cb2ca71048bef6d","identifiers":[["shelly","B8D61A8A7508"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 1 UL","model_id":"SNSW-001X15UL","modified_at":"2025-03-20T15:46:20.018112+00:00","name_by_user":null,"name":"shellyplus1-b8d61a8a7508","primary_config_entry":"01JCEH2A2K8Y9VEK9XV8ZPC69H","serial_number":null,"sw_version":"20250318-152131/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["01JD0G9D9ZXBC64EXQHJXE9AEQ"],"config_entries_subentries":{"01JD0G9D9ZXBC64EXQHJXE9AEQ":[null]},"configuration_url":"http://192.168.1.213:80","connections":[["mac","e8:6b:ea:e4:73:74"]],"created_at":"2024-11-18T20:44:08.027219+00:00","disabled_by":null,"entry_type":null,"hw_version":"gen2","id":"c737f87bdbd8f7d053768bd9e117eb77","identifiers":[["shelly","E86BEAE47374"]],"labels":[],"manufacturer":"Shelly","model":"Shelly Plus 0-10V Dimmer","model_id":"SNDM-00100WW","modified_at":"2025-03-28T16:50:12.354583+00:00","name_by_user":null,"name":"shellyplus010v-e86beae47374","primary_config_entry":"01JD0G9D9ZXBC64EXQHJXE9AEQ","serial_number":null,"sw_version":"20250318-152134/1.5.1-g01dd7ff","via_device_id":null},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/67a4e50a0161d803e4000713","connections":[["mac","28:70:4e:13:0b:20"]],"created_at":"2025-02-06T16:36:26.444883+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"a371935516f75019d9202827d7ef3687","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.229267+00:00","name_by_user":null,"name":"G5 Flex 03","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/67a4e60f03a0d803e400079d","connections":[["mac","28:70:4e:13:0a:c8"]],"created_at":"2025-02-06T16:40:48.009984+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"a2c601d0660b01099a56a85487019dd1","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.230784+00:00","name_by_user":null,"name":"G5 Flex 04","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/67a4e879012bd803e4000908","connections":[["mac","28:70:4e:13:0a:e8"]],"created_at":"2025-02-06T16:51:05.423024+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"4e74c75d01e805bbc0f4915d697dc400","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.229033+00:00","name_by_user":null,"name":"G5 Flex 05","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":"homeassistant://hassio/addon/cb646a50_get","connections":[],"created_at":"2024-10-22T13:47:35.159933+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"b75d4a683512046d198747fd8f4f8d55","identifiers":[["hassio","cb646a50_get"]],"labels":[],"manufacturer":"HACS Add-ons Repository","model":"Home Assistant Add-on","model_id":null,"modified_at":"2025-02-17T17:27:42.631347+00:00","name_by_user":null,"name":"Get HACS","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"1.3.1","via_device_id":null},
{"area_id":null,"config_entries":["01JKXM91D1BNRC2505EHYRESCW"],"config_entries_subentries":{"01JKXM91D1BNRC2505EHYRESCW":[null]},"configuration_url":null,"connections":[["bluetooth","CC:7B:5C:0D:0E:B6"]],"created_at":"2025-02-12T17:45:59.458394+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"584b8dd35655c0a34f2154293a1d4c82","identifiers":[],"labels":[],"manufacturer":"Espressif Inc. (shelly)","model":"SNSW-001X15UL","model_id":null,"modified_at":"2025-04-08T16:25:25.578351+00:00","name_by_user":null,"name":"Sheller Drum Enable (CC:7B:5C:0D:0E:B6)","primary_config_entry":"01JKXM91D1BNRC2505EHYRESCW","serial_number":null,"sw_version":null,"via_device_id":"98bc66339afcb5b0ee18bd93df9ab0f0"},
{"area_id":null,"config_entries":["01JKXMMK0K5E1EZJ90XJCAKPBB"],"config_entries_subentries":{"01JKXMMK0K5E1EZJ90XJCAKPBB":[null]},"configuration_url":"homeassistant://hacs","connections":[],"created_at":"2024-10-22T13:49:17.215255+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"9ea8fbfa22ebda2250a4a9abb50877e0","identifiers":[["hacs","0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd"]],"labels":[],"manufacturer":"hacs.xyz","model":"","model_id":null,"modified_at":"2025-04-30T15:58:59.637649+00:00","name_by_user":null,"name":"HACS","primary_config_entry":"01JKXMMK0K5E1EZJ90XJCAKPBB","serial_number":null,"sw_version":"2.0.5","via_device_id":null},
{"area_id":null,"config_entries":["01JKXMMK0K5E1EZJ90XJCAKPBB"],"config_entries_subentries":{"01JKXMMK0K5E1EZJ90XJCAKPBB":[null]},"configuration_url":"homeassistant://hacs/repository/700780425","connections":[],"created_at":"2024-11-15T19:02:20.334863+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"6cc029fc86b718ccb3c24c16f7b92fab","identifiers":[["hacs","700780425"]],"labels":[],"manufacturer":"jekalmin","model":"integration","model_id":null,"modified_at":"2025-04-30T15:58:59.637568+00:00","name_by_user":null,"name":"extended_openai_conversation","primary_config_entry":"01JKXMMK0K5E1EZJ90XJCAKPBB","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":null,"connections":[],"created_at":"2024-09-30T14:21:50.145121+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"db658cf1b7110495bd574ecbfea00f65","identifiers":[["hassio","OS"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Operating System","model_id":null,"modified_at":"2025-04-15T19:07:09.268572+00:00","name_by_user":null,"name":"Home Assistant Operating System","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"15.2","via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":"homeassistant://hassio/addon/5c53de3b_esphome","connections":[],"created_at":"2025-02-17T18:53:46.200555+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"f8e2640249eaf8eef9cc69e25428274b","identifiers":[["hassio","5c53de3b_esphome"]],"labels":[],"manufacturer":"ESPHome","model":"Home Assistant Add-on","model_id":null,"modified_at":"2025-04-30T15:19:50.230036+00:00","name_by_user":null,"name":"ESPHome Device Builder","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"2025.4.1","via_device_id":null},
{"area_id":null,"config_entries":["01JKXMMK0K5E1EZJ90XJCAKPBB"],"config_entries_subentries":{"01JKXMMK0K5E1EZJ90XJCAKPBB":[null]},"configuration_url":"homeassistant://hacs/repository/445609628","connections":[],"created_at":"2024-11-19T16:24:46.160618+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"e513618baa485a8b9b14e6d504e35ce1","identifiers":[["hacs","445609628"]],"labels":[],"manufacturer":"Soloam","model":"integration","model_id":null,"modified_at":"2025-04-30T15:58:59.637236+00:00","name_by_user":null,"name":"PID Controller","primary_config_entry":"01JKXMMK0K5E1EZJ90XJCAKPBB","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01JT3VZ248SG5C78NYX10R3VYC"],"config_entries_subentries":{"01JT3VZ248SG5C78NYX10R3VYC":[null]},"configuration_url":null,"connections":[],"created_at":"2025-03-10T19:44:30.863385+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"dcf6caf077557f82b96ad86995b9fd59","identifiers":[["mobile_app","98A9293F-CB61-45EC-B9B6-192CF169CDAD"]],"labels":[],"manufacturer":"Apple","model":"iPad12,1","model_id":null,"modified_at":"2025-04-30T17:29:58.667490+00:00","name_by_user":null,"name":"Factorys iPad","primary_config_entry":"01JT3VZ248SG5C78NYX10R3VYC","serial_number":null,"sw_version":"15.5","via_device_id":null},
{"area_id":null,"config_entries":["01JKXMMK0K5E1EZJ90XJCAKPBB"],"config_entries_subentries":{"01JKXMMK0K5E1EZJ90XJCAKPBB":[null]},"configuration_url":"homeassistant://hacs/repository/194140521","connections":[],"created_at":"2024-10-22T13:49:51.003152+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"62403f88b1f1defa8b35404d6c8b1519","identifiers":[["hacs","194140521"]],"labels":[],"manufacturer":"thomasloven","model":"integration","model_id":null,"modified_at":"2025-04-30T15:58:59.637383+00:00","name_by_user":null,"name":"browser_mod","primary_config_entry":"01JKXMMK0K5E1EZJ90XJCAKPBB","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01JAT8AZHNSF6WPATHNE5D6XM4"],"config_entries_subentries":{"01JAT8AZHNSF6WPATHNE5D6XM4":[null]},"configuration_url":null,"connections":[],"created_at":"2025-03-10T20:13:06.573887+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"73396d1a6a0c2edf200915f3d71c03df","identifiers":[["browser_mod","Ingest iPad"]],"labels":[],"manufacturer":"Browser Mod","model":null,"model_id":null,"modified_at":"2025-03-10T20:13:06.574104+00:00","name_by_user":null,"name":"Ingest iPad","primary_config_entry":"01JAT8AZHNSF6WPATHNE5D6XM4","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01JKXMMK0K5E1EZJ90XJCAKPBB"],"config_entries_subentries":{"01JKXMMK0K5E1EZJ90XJCAKPBB":[null]},"configuration_url":"homeassistant://hacs/repository/755918775","connections":[],"created_at":"2025-03-11T13:46:12.044435+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"a232aae81a9efd67ebff8c2f0c6505ab","identifiers":[["hacs","755918775"]],"labels":[],"manufacturer":"EuleMitKeule","model":"integration","model_id":null,"modified_at":"2025-04-30T15:58:59.637725+00:00","name_by_user":null,"name":"Device Tools","primary_config_entry":"01JKXMMK0K5E1EZJ90XJCAKPBB","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":"jc_machine","config_entries":["01JP2QDHQY8CAXX25PYRGQRD4E","01J5NH0A41TYMRRGASE2QJ0SQ5"],"config_entries_subentries":{"01JP2QDHQY8CAXX25PYRGQRD4E":[null],"01J5NH0A41TYMRRGASE2QJ0SQ5":[null]},"configuration_url":null,"connections":[],"created_at":"2025-03-11T13:48:33.800889+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"bd08feb9232d2cb49bb7742ac8a6bd13","identifiers":[["device_tools","01JP2QDHQY8CAXX25PYRGQRD4E"]],"labels":[],"manufacturer":"ME&E","model":"JC","model_id":null,"modified_at":"2025-03-11T14:44:01.725284+00:00","name_by_user":null,"name":"JC Cracker","primary_config_entry":"01JP2QDHQY8CAXX25PYRGQRD4E","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":"meyer_machine","config_entries":["01JP2RT3TC42WQMQX86W8PBYGR"],"config_entries_subentries":{"01JP2RT3TC42WQMQX86W8PBYGR":[null]},"configuration_url":null,"connections":[],"created_at":"2025-03-11T14:12:51.485092+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"7fb5d449ee7e9b4b92969c8006ef3f8d","identifiers":[["device_tools","01JP2RT3TC42WQMQX86W8PBYGR"]],"labels":[],"manufacturer":"ME&E","model":"Meyer","model_id":null,"modified_at":"2025-03-11T14:44:11.799766+00:00","name_by_user":null,"name":"Meyer Cracker","primary_config_entry":"01JP2RT3TC42WQMQX86W8PBYGR","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":"sheller_machine","config_entries":["04978edcf23c54a047e4f421779754ad","779bd6f1f6eebd9fb67b45fa40386e0c","51355cd442e2d0c51a3a43811555ee77","ce337fdb50b165d7ba080505d5c73343","01JP2RYPC08P80S3KGJGNQTG5B","143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"01JP2RYPC08P80S3KGJGNQTG5B":[null],"04978edcf23c54a047e4f421779754ad":[null],"ce337fdb50b165d7ba080505d5c73343":[null],"779bd6f1f6eebd9fb67b45fa40386e0c":[null],"51355cd442e2d0c51a3a43811555ee77":[null],"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-03-11T14:15:21.599606+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"ac5aeecdb81f624f67d67ca7cd25d696","identifiers":[["device_tools","01JP2RYPC08P80S3KGJGNQTG5B"]],"labels":[],"manufacturer":"ME&E","model":"14\" Sheller","model_id":null,"modified_at":"2025-03-11T14:44:26.816716+00:00","name_by_user":null,"name":"Sheller Machine","primary_config_entry":"01JP2RYPC08P80S3KGJGNQTG5B","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":"homeassistant://hassio/addon/core_configurator","connections":[],"created_at":"2025-03-24T16:13:16.077174+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"5dc30d37c1bf60fa9300c60997f41124","identifiers":[["hassio","core_configurator"]],"labels":[],"manufacturer":"Official add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2025-03-24T16:13:16.077376+00:00","name_by_user":null,"name":"File editor","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"5.8.0","via_device_id":null},
{"area_id":null,"config_entries":["01JRB3H9PGGBS0Y87YRVZFBN7S"],"config_entries_subentries":{"01JRB3H9PGGBS0Y87YRVZFBN7S":[null]},"configuration_url":"homeassistant://config/backup","connections":[],"created_at":"2025-04-08T16:25:33.652452+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"833d67c5ea71b9e75c69ce70ce2b86e3","identifiers":[["backup","backup_manager"]],"labels":[],"manufacturer":"Home Assistant","model":"Home Assistant Backup","model_id":null,"modified_at":"2025-04-28T15:39:46.109960+00:00","name_by_user":null,"name":"Backup","primary_config_entry":"01JRB3H9PGGBS0Y87YRVZFBN7S","serial_number":null,"sw_version":"2025.4.4","via_device_id":null},
{"area_id":null,"config_entries":["01JKXMMK0K5E1EZJ90XJCAKPBB"],"config_entries_subentries":{"01JKXMMK0K5E1EZJ90XJCAKPBB":[null]},"configuration_url":"homeassistant://hacs/repository/202220932","connections":[],"created_at":"2025-04-08T22:09:31.339524+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"81de29d058e241575299f4994a995c2b","identifiers":[["hacs","202220932"]],"labels":[],"manufacturer":"thomasloven","model":"integration","model_id":null,"modified_at":"2025-04-30T15:58:59.637482+00:00","name_by_user":null,"name":"Favicon changer","primary_config_entry":"01JKXMMK0K5E1EZJ90XJCAKPBB","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":"jc_machine","config_entries":["01JS27E8649HW777MJ0KF33QA2"],"config_entries_subentries":{"01JS27E8649HW777MJ0KF33QA2":[null]},"configuration_url":null,"connections":[["bluetooth","58:10:31:E7:7C:01"]],"created_at":"2025-04-17T15:56:20.046196+00:00","disabled_by":null,"entry_type":null,"hw_version":"usb:v1D6Bp0246d054F","id":"5cebfa2bd3196fd6bcbb13eaf8b06060","identifiers":[],"labels":[],"manufacturer":"Realtek","model":"Bluetooth Radio (0bda:0852)","model_id":null,"modified_at":"2025-04-17T15:56:24.903881+00:00","name_by_user":null,"name":"hci0 (58:10:31:E7:7C:01)","primary_config_entry":"01JS27E8649HW777MJ0KF33QA2","serial_number":null,"sw_version":"homeassistant","via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-17T18:03:13.001134+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"a3919ba3219a48fe522181e5d2bab2b9","identifiers":[["mqtt","barcode-scanner"]],"labels":[],"manufacturer":"Netum","model":"C750","model_id":null,"modified_at":"2025-04-17T18:03:13.001159+00:00","name_by_user":null,"name":"Barcode Tag Scanner","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01JS2N75XQZ4PBCK7CDCZ1X2KP"],"config_entries_subentries":{"01JS2N75XQZ4PBCK7CDCZ1X2KP":[null]},"configuration_url":"http://192.168.1.212","connections":[],"created_at":"2025-04-17T19:56:30.791864+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"309798ef61a8710c832940c131598d1b","identifiers":[["voip","sip:engr-ugaif@192.168.1.212:5060"]],"labels":[],"manufacturer":null,"model":"engr-ugaif","model_id":null,"modified_at":"2025-04-17T19:57:23.810258+00:00","name_by_user":null,"name":"192.168.1.212","primary_config_entry":"01JS2N75XQZ4PBCK7CDCZ1X2KP","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01JS2TPHY0WM1EHSFM6PP3N93A"],"config_entries_subentries":{"01JS2TPHY0WM1EHSFM6PP3N93A":[null]},"configuration_url":null,"connections":[["upnp","uuid:8d6c30fd-0475-44b8-9c12-466689f35e58"],["mac","f4:e2:c6:e4:d2:f6"]],"created_at":"2025-04-17T21:32:55.240642+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"0b7a672398058e36e4e48a37b3756762","identifiers":[["upnp_host","192.168.1.1"],["upnp","uuid:8d6c30fd-0475-44b8-9c12-466689f35e58::urn:schemas-upnp-org:device:InternetGatewayDevice:2"],["upnp_serial_number","f4:e2:c6:e4:d2:f6"]],"labels":[],"manufacturer":"Ubiquiti Networks","model":"UXG Lite","model_id":null,"modified_at":"2025-04-30T18:14:08.209676+00:00","name_by_user":null,"name":"UniFi NeXt-Gen Gateway","primary_config_entry":"01JS2TPHY0WM1EHSFM6PP3N93A","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/6806c4c101b40103e40e3c44","connections":[["mac","28:70:4e:17:70:ec"]],"created_at":"2025-04-21T22:20:49.529297+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"b8f6f84d89447912b6703e9e7fdd1664","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.229588+00:00","name_by_user":null,"name":"G5 Flex 06","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["55db3b46f3bf75777e4779fd25ed6bca"],"config_entries_subentries":{"55db3b46f3bf75777e4779fd25ed6bca":[null]},"configuration_url":"https://172.22.114.176/protect/devices/6806cda000570103e40e8640","connections":[["mac","28:70:4e:17:70:b7"]],"created_at":"2025-04-21T22:58:40.161827+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"e5423b32c524f6ad6157c8ca3a156cba","identifiers":[],"labels":[],"manufacturer":"Ubiquiti","model":"G5 Flex","model_id":"UVC G5 Flex","modified_at":"2025-04-28T15:39:46.230383+00:00","name_by_user":null,"name":"G5 Flex 07","primary_config_entry":"55db3b46f3bf75777e4779fd25ed6bca","serial_number":null,"sw_version":"4.75.62","via_device_id":"437dbead96a87ac711c7a4d99a79c6cb"},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-28T17:07:09.462690+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"abc30051787003e963ee34ee5baf9fc9","identifiers":[["mqtt","scanner_1"]],"labels":[],"manufacturer":"Netum","model":"C750","model_id":null,"modified_at":"2025-04-28T17:07:09.462725+00:00","name_by_user":null,"name":"Barcode Tag Scanner scanner_1","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-28T17:07:09.463333+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"da613b80ee3c2a0e321ba6cc1d18ef8f","identifiers":[["mqtt","scanner_2"]],"labels":[],"manufacturer":"Netum","model":"C750","model_id":null,"modified_at":"2025-04-28T17:07:09.463344+00:00","name_by_user":null,"name":"Barcode Tag Scanner scanner_2","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-28T17:07:09.464252+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"3924458c8a1bec286d9eabdb1bea7bb5","identifiers":[["mqtt","scanner_3"]],"labels":[],"manufacturer":"Netum","model":"DS-8100","model_id":null,"modified_at":"2025-04-28T17:07:09.464263+00:00","name_by_user":null,"name":"Barcode Tag Scanner scanner_3","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-29T19:21:32.491747+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"82395c211fed9b358b604c33ca0bea9e","identifiers":[["mqtt","scanner_4"]],"labels":[],"manufacturer":"Generic","model":"C200","model_id":null,"modified_at":"2025-04-29T20:26:14.633733+00:00","name_by_user":null,"name":"scanner_4","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-29T19:21:32.492879+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"c11499726c52d4e88f2d46ec6db288a6","identifiers":[["mqtt","scanner_5"]],"labels":[],"manufacturer":"Generic","model":"C200","model_id":null,"modified_at":"2025-04-29T20:26:14.634494+00:00","name_by_user":null,"name":"scanner_5","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null},
{"area_id":null,"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"configuration_url":"homeassistant://hassio/addon/a0d7b954_vscode","connections":[],"created_at":"2024-09-30T14:21:50.119745+00:00","disabled_by":null,"entry_type":"service","hw_version":null,"id":"4337d0ff05fb782c9ec2ae035488a00a","identifiers":[["hassio","a0d7b954_vscode"]],"labels":[],"manufacturer":"Home Assistant Community Add-ons","model":"Home Assistant Add-on","model_id":null,"modified_at":"2025-04-29T19:49:42.628821+00:00","name_by_user":null,"name":"Studio Code Server","primary_config_entry":"01J91MY5JBJH64GQS4WA5SYVK2","serial_number":null,"sw_version":"5.19.2","via_device_id":null},
{"area_id":null,"config_entries":["143eb40c5189f32be0eddf773eaaeceb"],"config_entries_subentries":{"143eb40c5189f32be0eddf773eaaeceb":[null]},"configuration_url":null,"connections":[],"created_at":"2025-04-29T20:26:14.674232+00:00","disabled_by":null,"entry_type":null,"hw_version":null,"id":"e6057a6d16d6d00efb7d6039743c2548","identifiers":[["mqtt","scanner_6"]],"labels":[],"manufacturer":"Generic","model":"C200","model_id":null,"modified_at":"2025-04-29T20:26:14.674247+00:00","name_by_user":null,"name":"scanner_6","primary_config_entry":"143eb40c5189f32be0eddf773eaaeceb","serial_number":null,"sw_version":null,"via_device_id":null}
], ],
"deleted_devices": [] "deleted_devices": [
{"config_entries":["01JAT8AZHNSF6WPATHNE5D6XM4"],"config_entries_subentries":{"01JAT8AZHNSF6WPATHNE5D6XM4":[null]},"connections":[],"created_at":"2024-10-22T14:25:00.929774+00:00","identifiers":[["browser_mod","fcc41464-a82af7ed"]],"id":"a9c5ef824d4d18dc6dc5b5ac532ca56f","orphaned_timestamp":null,"modified_at":"2024-10-22T14:25:11.023598+00:00"},
{"config_entries":["01JAT8AZHNSF6WPATHNE5D6XM4"],"config_entries_subentries":{"01JAT8AZHNSF6WPATHNE5D6XM4":[null]},"connections":[],"created_at":"2024-10-22T14:39:00.251130+00:00","identifiers":[["browser_mod","CAST"]],"id":"8a4ea6b3c054155a422487206c6f9721","orphaned_timestamp":null,"modified_at":"2024-10-22T14:39:04.566708+00:00"},
{"config_entries":["01J91MY5JBJH64GQS4WA5SYVK2"],"config_entries_subentries":{"01J91MY5JBJH64GQS4WA5SYVK2":[null]},"connections":[],"created_at":"2025-03-14T14:46:55.910012+00:00","identifiers":[["hassio","a0d7b954_grafana"]],"id":"784a7de235c6a7e30bd8d0e3e216901b","orphaned_timestamp":null,"modified_at":"2025-03-20T15:45:06.991729+00:00"},
{"config_entries":[],"config_entries_subentries":{},"connections":[],"created_at":"2024-11-15T19:12:44.589247+00:00","identifiers":[["voip","sip:IPCall@192.168.1.211:5060"]],"id":"8582e1111c8603bb324b69215f997ccb","orphaned_timestamp":1744918805.925204,"modified_at":"2025-04-17T19:40:05.925227+00:00"},
{"config_entries":["01JCRJQQPN6G7D20NFABPG3Y3V"],"config_entries_subentries":{"01JCRJQQPN6G7D20NFABPG3Y3V":[null]},"connections":[],"created_at":"2024-11-15T18:52:58.976862+00:00","identifiers":[["openai_conversation","01JCRJQQPN6G7D20NFABPG3Y3V"]],"id":"ccab8330c09c9c861e5585070e06578b","orphaned_timestamp":null,"modified_at":"2025-04-17T19:40:15.927029+00:00"},
{"config_entries":["01JKR7TM57W6HDZWKBEZQ859MJ"],"config_entries_subentries":{"01JKR7TM57W6HDZWKBEZQ859MJ":[null]},"connections":[],"created_at":"2025-02-10T15:32:12.084456+00:00","identifiers":[["openai_conversation","01JKR7TM57W6HDZWKBEZQ859MJ"]],"id":"b5e22a86624b56f6a2e92ca2bf634bd4","orphaned_timestamp":null,"modified_at":"2025-04-17T19:40:15.927277+00:00"},
{"config_entries":[],"config_entries_subentries":{},"connections":[],"created_at":"2025-04-17T19:39:19.654863+00:00","identifiers":[["voip","sip:IPCall@192.168.1.212:5060"]],"id":"719560bae4c6a4753d29d5ce0a4b5441","orphaned_timestamp":1744919825.216603,"modified_at":"2025-04-17T19:57:05.216620+00:00"}
]
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
{
"version": 1,
"minor_version": 1,
"key": "esphome.dashboard",
"data": {
"info": {
"addon_slug": "5c53de3b_esphome",
"host": "127.0.0.1",
"port": 65155
}
}
}

13
.storage/hacs.critical Normal file
View File

@ -0,0 +1,13 @@
{
"version": "6",
"minor_version": 1,
"key": "hacs.critical",
"data": [
{
"repository": "test/test",
"reason": "Security issues, known to steal auth tokens.",
"link": "https://github.com/hacs/default/pull/2",
"acknowledged": true
}
]
}

8633
.storage/hacs.data Normal file

File diff suppressed because it is too large Load Diff

10
.storage/hacs.hacs Normal file
View File

@ -0,0 +1,10 @@
{
"version": "6",
"minor_version": 1,
"key": "hacs.hacs",
"data": {
"archived_repositories": [],
"renamed_repositories": {},
"ignored_repositories": []
}
}

28116
.storage/hacs.repositories Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,17 +3,17 @@
"minor_version": 1, "minor_version": 1,
"key": "http", "key": "http",
"data": { "data": {
"ssl_profile": "modern",
"ip_ban_enabled": true, "ip_ban_enabled": true,
"cors_allowed_origins": [ "ssl_profile": "modern",
"https://cast.home-assistant.io"
],
"server_port": 8123,
"server_host": [ "server_host": [
"0.0.0.0", "0.0.0.0",
"::" "::"
], ],
"cors_allowed_origins": [
"https://cast.home-assistant.io"
],
"use_x_frame_options": true, "use_x_frame_options": true,
"server_port": 8123,
"login_attempts_threshold": -1 "login_attempts_threshold": -1
} }
} }

View File

@ -3,6 +3,31 @@
"minor_version": 1, "minor_version": 1,
"key": "input_number", "key": "input_number",
"data": { "data": {
"items": [] "items": [
{
"id": "jc_rolling_pecan_sum",
"min": 0.0,
"max": 1000000.0,
"name": "JC Rolling Pecan Sum",
"mode": "slider",
"step": 1.0
},
{
"id": "jc_rate_prev_error",
"min": -40.0,
"max": 40.0,
"name": "JC Rate Prev Error",
"mode": "slider",
"step": 1.0
},
{
"id": "jc_rate_prev_adjustment",
"min": -40.0,
"max": 40.0,
"name": "JC Rate Prev Adjustment",
"mode": "slider",
"step": 1.0
}
]
} }
} }

View File

@ -11,7 +11,9 @@
"options": [ "options": [
"General Yield Sample", "General Yield Sample",
"Half Yield Sample", "Half Yield Sample",
"Tare" "Tare",
"Wet Mass Sample",
"Dry Mass Sample"
] ]
}, },
{ {
@ -37,6 +39,38 @@
"15", "15",
"16" "16"
] ]
},
{
"id": "activesample",
"name": "ActiveSample",
"icon": "mdi:scale-balance",
"options": [
"None",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
"22",
"23",
"24"
]
} }
] ]
} }

View File

@ -0,0 +1,25 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.ayoub_testing",
"data": {
"config": {
"views": [
{
"title": "Home",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "New section"
}
]
}
]
}
]
}
}
}

View File

@ -0,0 +1,15 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.dashboard_ingest",
"data": {
"config": {
"views": [
{
"title": "Home",
"sections": []
}
]
}
}
}

View File

@ -54,6 +54,9 @@
{ {
"entity": "input_number.jc_crush_amount", "entity": "input_number.jc_crush_amount",
"name": "Select Crush Amount" "name": "Select Crush Amount"
},
{
"entity": "number.jc_angle"
} }
] ]
} }
@ -62,13 +65,22 @@
{ {
"type": "entities", "type": "entities",
"entities": [ "entities": [
{
"entity": "switch.jc_run_control"
},
{ {
"entity": "switch.shellyplus1pm_c049ef8c7310_switch_0", "entity": "switch.shellyplus1pm_c049ef8c7310_switch_0",
"secondary_info": "none", "secondary_info": "none",
"name": "Vibratory Feed Enable" "name": "Vibratory Feed Enable"
}, },
{
"entity": "input_number.jc_plate_frequency"
},
{ {
"entity": "switch.tp_link_power_strip_d7c1_vibratory_conveyor" "entity": "switch.tp_link_power_strip_d7c1_vibratory_conveyor"
},
{
"entity": "switch.shellyplus1_b8d61a87d2a8_switch_0"
} }
], ],
"show_header_toggle": false "show_header_toggle": false
@ -76,24 +88,16 @@
{ {
"type": "entities", "type": "entities",
"entities": [ "entities": [
"number.jc_feed_time", {
"switch.jc_limit_feed_duration" "entity": "sensor.jc_throughput_count"
]
}, },
{ {
"type": "entities", "entity": "number.jc_feedrate_setpoint"
"entities": [
{
"entity": "input_number.jc_plate_frequency"
}, },
{ {
"entity": "input_number.jc_feeder_frequency" "entity": "sensor.jc_pi_controller_output"
},
{
"entity": "input_number.batch_weight"
} }
], ]
"title": "Set Manually on Machine (ONLY FOR DATA LOGGING)"
} }
] ]
} }

View File

@ -39,15 +39,11 @@
"entity_id": "script.mqtt_disable_torque" "entity_id": "script.mqtt_disable_torque"
} }
} }
}
]
}, },
{ {
"graph": "line", "entity": "input_number.meyer_screw_displacement"
"type": "sensor", }
"entity": "sensor.meyer_position_raw", ]
"name": "Current Position",
"detail": 1
}, },
{ {
"type": "horizontal-stack", "type": "horizontal-stack",
@ -181,7 +177,13 @@
{ {
"type": "entities", "type": "entities",
"entities": [ "entities": [
"switch.tp_link_power_strip_d7c1_vibratory_conveyor" {
"entity": "switch.run_control",
"name": "Meyer Enable"
},
{
"entity": "switch.tp_link_power_strip_d7c1_vibratory_conveyor"
}
] ]
} }
] ]

View File

@ -33,6 +33,15 @@
"entity": "sensor.steinlite_sample_temperature" "entity": "sensor.steinlite_sample_temperature"
} }
] ]
},
{
"type": "entities",
"entities": [
{
"entity": "sensor.jc_moisttech_pecan_moisture"
}
],
"title": "JC MoistTech Values"
} }
] ]
} }

View File

@ -43,7 +43,7 @@
}, },
{ {
"type": "entity", "type": "entity",
"entity": "sensor.sheller_scale", "entity": "sensor.sheller_scale_stable",
"name": "Sheller Scale", "name": "Sheller Scale",
"icon": "mdi:scale" "icon": "mdi:scale"
} }
@ -58,7 +58,7 @@
"cards": [ "cards": [
{ {
"type": "entity", "type": "entity",
"entity": "sensor.precision_scale", "entity": "sensor.precision_scale_stable",
"name": "Precision Scale", "name": "Precision Scale",
"icon": "mdi:scale" "icon": "mdi:scale"
}, },

View File

@ -37,10 +37,12 @@
{ {
"type": "entities", "type": "entities",
"entities": [ "entities": [
"switch.shellyplus1_cc7b5c0d0eb4_switch_0", {
"light.shellyplus010v_e86beae4d350_light_0", "entity": "switch.shellyplus1_cc7b5c0d0eb4_switch_0"
"switch.shellyplus1_cc7b5c0d316c_switch_0", },
"light.shellyplus010v_e86beae4df24_light_0" {
"entity": "switch.shellyplus1_cc7b5c0d316c_switch_0"
}
] ]
}, },
{ {
@ -49,6 +51,21 @@
"sensor.shelling_machine_drum_rpm", "sensor.shelling_machine_drum_rpm",
"sensor.shelling_machine_paddle_rpm" "sensor.shelling_machine_paddle_rpm"
] ]
},
{
"type": "entities",
"entities": [
{
"entity": "switch.shellyplus1_b8d61a87d2a8_switch_0"
},
{
"entity": "switch.shellyplus1_b8d61a8a7508_switch_0"
},
{
"entity": "switch.tp_link_power_strip_d7c1_vibratory_conveyor",
"name": "Cracker Output Vibratory Conveyor"
}
]
} }
] ]
} }

View File

@ -0,0 +1,49 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.moisture_scales",
"data": {
"config": {
"views": [
{
"title": "Overview",
"cards": [
{
"title": "Precision Scale",
"type": "vertical-stack",
"cards": [
{
"type": "horizontal-stack",
"cards": [
{
"type": "entity",
"entity": "sensor.precision_scale_stable",
"name": "Precision Scale",
"icon": "mdi:scale"
},
{
"type": "entity",
"entity": "input_select.activesample",
"icon": "mdi:weight-gram"
},
{
"type": "entity",
"entity": "input_select.mass_sample_mode",
"icon": "mdi:weight"
}
]
},
{
"type": "entity",
"icon": "mdi:label-percent-outline",
"entity": "sensor.latest_moisture_sample_by_weight"
}
]
}
],
"type": "sidebar"
}
]
}
}
}

View File

@ -57,6 +57,33 @@
"require_admin": false, "require_admin": false,
"show_in_sidebar": true, "show_in_sidebar": true,
"mode": "storage" "mode": "storage"
},
{
"id": "moisture_scales",
"show_in_sidebar": true,
"icon": "mdi:thermometer-water",
"title": "Moisture Scales",
"require_admin": false,
"mode": "storage",
"url_path": "moisture-scales"
},
{
"id": "ayoub_testing",
"show_in_sidebar": true,
"icon": "mdi:test-tube",
"title": "Ayoub Testing",
"require_admin": false,
"mode": "storage",
"url_path": "ayoub-testing"
},
{
"id": "dashboard_ingest",
"show_in_sidebar": true,
"icon": "mdi:download-multiple-outline",
"title": "Ingest",
"require_admin": false,
"mode": "storage",
"url_path": "dashboard-ingest"
} }
] ]
} }

View File

@ -0,0 +1,24 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace_resources",
"data": {
"items": [
{
"id": "a93a7b692b1f4df8940975f3599a4a5f",
"url": "/hacsfiles/fullscreen-card/fullscreen-card.js?hacstag=29028126708",
"type": "module"
},
{
"id": "6b0163845db0475c8163c3f6363f32bd",
"url": "/hacsfiles/homeassistant-browser-control-card/browser-control-card.js?hacstag=452251255140",
"type": "module"
},
{
"id": "26160932f8534a2d930da18b23be0192",
"url": "/browser_mod.js?automatically-added",
"type": "module"
}
]
}
}

8219
.storage/mobile_app Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@
"name": "Pecan Lab", "name": "Pecan Lab",
"user_id": "5ef2c8c082b14074a6e84da694ef2f35", "user_id": "5ef2c8c082b14074a6e84da694ef2f35",
"device_trackers": [ "device_trackers": [
"device_tracker.lab_phone" "device_tracker.lab_phone",
"device_tracker.factorys_ipad"
] ]
} }
] ]

View File

@ -62,10 +62,52 @@
}, },
{ {
"created": "2024-10-01T13:01:00.227210+00:00", "created": "2024-10-01T13:01:00.227210+00:00",
"dismissed_version": null, "dismissed_version": "2024.9.3",
"domain": "mqtt", "domain": "mqtt",
"is_persistent": false, "is_persistent": false,
"issue_id": "payload_template_deprecation_/jc/height" "issue_id": "payload_template_deprecation_/jc/height"
},
{
"created": "2024-10-22T13:49:50.991576+00:00",
"dismissed_version": null,
"domain": "hacs",
"is_persistent": false,
"issue_id": "restart_required_194140521_tags/v2.3.1"
},
{
"created": "2024-11-12T18:56:33.104642+00:00",
"dismissed_version": null,
"domain": "modbus",
"is_persistent": false,
"issue_id": "deprecated_restart"
},
{
"created": "2024-11-13T16:33:06.238133+00:00",
"dismissed_version": null,
"domain": "modbus",
"is_persistent": false,
"issue_id": "duplicate_entity_name"
},
{
"created": "2024-12-18T18:29:06.178442+00:00",
"dismissed_version": null,
"domain": "hacs",
"is_persistent": false,
"issue_id": "restart_required_152294445_tags/4.5"
},
{
"created": "2025-03-08T23:03:51.211687+00:00",
"dismissed_version": null,
"domain": "hassio",
"is_persistent": false,
"issue_id": "9936e4e952644082be72d7975b6cd64a"
},
{
"created": "2025-03-10T19:45:47.722481+00:00",
"dismissed_version": null,
"domain": "hacs",
"is_persistent": false,
"issue_id": "restart_required_194140521_tags/v2.3.3"
} }
] ]
} }

View File

@ -6,70 +6,60 @@
"items": [ "items": [
{ {
"id": "1B983A04", "id": "1B983A04",
"name": "take-halves-mass-sample", "last_scanned": "2025-04-30T16:13:50.356724+00:00",
"last_scanned": "2024-09-30T18:44:49.661014+00:00",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "1ADE4304", "id": "1ADE4304",
"name": "take-general-mass-sample", "last_scanned": "2025-04-30T18:26:55.891929+00:00",
"last_scanned": "2024-09-30T18:53:09.940972+00:00",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "1AECA004", "id": "1AECA004",
"name": "bin1a", "last_scanned": "2024-11-06T19:41:23.031062+00:00",
"last_scanned": "2024-07-19T17:27:19.039906+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "feec748cc156a1ca441d38caf620ecfe"
}, },
{ {
"id": "1983D704", "id": "1983D704",
"name": "bin1b", "last_scanned": "2025-04-29T17:04:11.934582+00:00",
"last_scanned": "2024-09-13T16:09:14.338395+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "feec748cc156a1ca441d38caf620ecfe"
}, },
{ {
"id": "72F8A704", "id": "72F8A704",
"name": "bin2a", "last_scanned": "2025-04-29T17:04:43.671901+00:00",
"last_scanned": "2024-09-13T16:09:42.455356+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "feec748cc156a1ca441d38caf620ecfe"
}, },
{ {
"id": "1A414204", "id": "1A414204",
"name": "bin2b", "last_scanned": "2024-10-30T21:21:31.399347+00:00",
"last_scanned": "2024-09-11T15:29:01.967216+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "feec748cc156a1ca441d38caf620ecfe"
}, },
{ {
"id": "17D18B04", "id": "17D18B04",
"name": "bin3a", "last_scanned": "2025-04-29T17:04:55.241504+00:00",
"last_scanned": "2024-09-13T16:10:11.571487+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "feec748cc156a1ca441d38caf620ecfe"
}, },
{ {
"id": "1B9E6204", "id": "1B9E6204",
"name": "bin3b",
"last_scanned": "2024-06-05T15:08:47.505079+00:00", "last_scanned": "2024-06-05T15:08:47.505079+00:00",
"migrated": true "migrated": true
}, },
{ {
"id": "1699DD04", "id": "1699DD04",
"name": "bin4a", "last_scanned": "2025-04-30T18:26:05.528169+00:00",
"last_scanned": "2024-09-30T18:48:52.498310+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "82395c211fed9b358b604c33ca0bea9e"
}, },
{ {
"id": "1DD6A704", "id": "1DD6A704",
"name": "bin4b", "last_scanned": "2025-04-29T17:02:18.547200+00:00",
"last_scanned": "2024-09-18T16:23:44.615496+00:00",
"migrated": true, "migrated": true,
"device_id": "feec748cc156a1ca441d38caf620ecfe" "device_id": "feec748cc156a1ca441d38caf620ecfe"
}, },
@ -85,120 +75,105 @@
}, },
{ {
"id": "1A4E4B04", "id": "1A4E4B04",
"last_scanned": "2024-05-09T15:03:42.985968+00:00", "last_scanned": "2025-04-17T20:43:29.034953+00:00",
"migrated": true "migrated": true,
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
}, },
{ {
"id": "11275204", "id": "11275204",
"last_scanned": "2024-09-30T18:45:50.522348+00:00", "last_scanned": "2025-04-30T16:14:14.886967+00:00",
"name": "Cup 1",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "123E8904", "id": "123E8904",
"last_scanned": "2024-09-30T18:46:04.951017+00:00", "last_scanned": "2025-04-29T17:34:12.369552+00:00",
"name": "Cup 2",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "3924458c8a1bec286d9eabdb1bea7bb5"
}, },
{ {
"id": "1FD4D704", "id": "1FD4D704",
"last_scanned": "2024-09-30T18:46:11.337333+00:00", "last_scanned": "2025-04-30T18:30:48.283286+00:00",
"name": "Cup 3",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "12619704", "id": "12619704",
"last_scanned": "2024-09-30T18:46:40.483063+00:00", "last_scanned": "2025-04-30T16:20:51.084691+00:00",
"name": "Cup 4",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "105B1904", "id": "105B1904",
"last_scanned": "2024-09-30T18:45:56.943239+00:00", "last_scanned": "2025-04-30T16:03:19.637582+00:00",
"name": "Cup 5",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "10A49304", "id": "10A49304",
"last_scanned": "2024-09-30T18:45:01.668428+00:00", "last_scanned": "2025-04-29T17:34:55.364348+00:00",
"name": "Cup 6",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "3924458c8a1bec286d9eabdb1bea7bb5"
}, },
{ {
"id": "1145FB04", "id": "1145FB04",
"last_scanned": "2024-09-30T18:46:26.264244+00:00", "last_scanned": "2025-04-29T17:33:13.903827+00:00",
"name": "Cup 7",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "3924458c8a1bec286d9eabdb1bea7bb5"
}, },
{ {
"id": "11866104", "id": "11866104",
"last_scanned": "2024-09-30T18:46:19.747487+00:00", "last_scanned": "2025-04-29T17:33:46.197532+00:00",
"name": "Cup 8",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "3924458c8a1bec286d9eabdb1bea7bb5"
}, },
{ {
"id": "1FBCF604", "id": "1FBCF604",
"last_scanned": "2024-09-30T18:46:48.985172+00:00", "last_scanned": "2025-04-30T16:13:51.195542+00:00",
"name": "Cup 9",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "11261C04", "id": "11261C04",
"last_scanned": "2024-09-30T18:45:09.658757+00:00", "last_scanned": "2025-04-30T16:03:59.977236+00:00",
"name": "Cup 10",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "116CE504", "id": "116CE504",
"last_scanned": "2024-09-30T18:45:26.180399+00:00", "last_scanned": "2025-04-30T16:14:48.717617+00:00",
"name": "Cup 11",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "10192204", "id": "10192204",
"last_scanned": "2024-09-30T18:45:18.037085+00:00", "last_scanned": "2025-04-30T16:15:23.648372+00:00",
"name": "Cup 12",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "1051FF04", "id": "1051FF04",
"last_scanned": "2024-09-30T18:46:56.503381+00:00", "last_scanned": "2025-04-30T16:19:45.247773+00:00",
"name": "Cup 13",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "10E1A404", "id": "10E1A404",
"last_scanned": "2024-09-30T18:46:33.780378+00:00", "last_scanned": "2025-04-30T16:20:21.932297+00:00",
"name": "Cup 14",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "1FB63C04", "id": "1FB63C04",
"last_scanned": "2024-09-30T18:45:42.002772+00:00", "last_scanned": "2025-04-30T16:04:37.390274+00:00",
"name": "Cup 15",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "10F61E04", "id": "10F61E04",
"last_scanned": "2024-09-30T18:45:33.579954+00:00", "last_scanned": "2025-04-30T16:05:19.458571+00:00",
"name": "Cup 16",
"migrated": true, "migrated": true,
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "e6057a6d16d6d00efb7d6039743c2548"
}, },
{ {
"id": "BCEA6208", "id": "BCEA6208",
@ -209,6 +184,336 @@
"id": "5E7D5408", "id": "5E7D5408",
"last_scanned": "2024-08-05T17:07:58.912663+00:00", "last_scanned": "2024-08-05T17:07:58.912663+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545" "device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "126AC504",
"last_scanned": "2024-10-16T20:06:21.273277+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "1FD56B04",
"last_scanned": "2024-10-16T20:08:46.232202+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "12A61104",
"last_scanned": "2024-10-16T20:09:58.680406+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "12123E04",
"last_scanned": "2024-10-16T20:05:12.492842+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "1174DD04",
"last_scanned": "2024-10-16T20:05:22.681966+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "11A35B04",
"last_scanned": "2024-10-16T20:11:38.427013+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "10113604",
"last_scanned": "2024-10-16T20:05:51.523423+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "11397F04",
"last_scanned": "2024-10-16T20:09:30.779940+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "124B4E04",
"last_scanned": "2024-10-16T20:10:54.345613+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "11D64604",
"last_scanned": "2024-10-16T20:10:58.045273+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "10CC6C04",
"last_scanned": "2024-10-16T20:09:41.630994+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "112BB904",
"last_scanned": "2024-10-16T20:05:41.715114+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "1C997304",
"last_scanned": "2025-04-30T16:01:32.847095+00:00",
"device_id": "e6057a6d16d6d00efb7d6039743c2548"
},
{
"id": "101BF204",
"last_scanned": "2024-10-16T20:07:07.932103+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "10C8FE04",
"last_scanned": "2024-10-16T20:05:32.488955+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "11AADF04",
"last_scanned": "2024-10-16T20:08:13.854832+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "12359604",
"last_scanned": "2025-04-30T12:39:14.439061+00:00",
"device_id": "e6057a6d16d6d00efb7d6039743c2548"
},
{
"id": "1CF2EB04",
"last_scanned": "2024-10-16T20:08:35.301584+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "1252B404",
"last_scanned": "2025-04-30T17:03:56.595065+00:00",
"device_id": "e6057a6d16d6d00efb7d6039743c2548"
},
{
"id": "128BC104",
"last_scanned": "2024-10-16T20:11:27.597855+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "12923C04",
"last_scanned": "2024-10-16T20:08:03.605599+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "111E5704",
"last_scanned": "2024-10-16T20:11:14.641640+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "107D4B04",
"last_scanned": "2024-10-16T20:11:49.529152+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "128DD104",
"last_scanned": "2024-10-16T20:06:12.188014+00:00",
"device_id": "c9fd6d12a759980d491da079f9e3a545"
},
{
"id": "6ED8C60D",
"last_scanned": "2025-02-18T16:04:11.262696+00:00",
"device_id": "feec748cc156a1ca441d38caf620ecfe"
},
{
"id": "6DEB99FD",
"last_scanned": "2025-02-18T17:40:35.032864+00:00",
"device_id": "feec748cc156a1ca441d38caf620ecfe"
},
{
"id": "6ED8858D",
"last_scanned": "2025-02-18T17:52:21.641988+00:00",
"device_id": "feec748cc156a1ca441d38caf620ecfe"
},
{
"id": "6E51E56D",
"last_scanned": "2025-02-19T15:40:14.957160+00:00",
"device_id": "feec748cc156a1ca441d38caf620ecfe"
},
{
"id": "810112472622",
"last_scanned": "2025-04-28T17:15:15.683878+00:00",
"device_id": "da613b80ee3c2a0e321ba6cc1d18ef8f"
},
{
"id": "500033",
"last_scanned": "2025-04-17T20:59:48.751425+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500021",
"last_scanned": "2025-04-17T20:21:13.366104+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "043a981b6f6180",
"last_scanned": "2025-04-17T20:33:33.558767+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "0443de1a6f6180",
"last_scanned": "2025-04-17T20:33:42.392952+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "04e56c11bb2a81",
"last_scanned": "2025-04-17T20:33:57.366177+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "041ef610bb2a81",
"last_scanned": "2025-04-17T20:35:21.147841+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "10f61e04",
"last_scanned": "2025-04-17T20:41:23.785347+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500032",
"last_scanned": "2025-04-28T17:07:31.549866+00:00",
"device_id": "3924458c8a1bec286d9eabdb1bea7bb5"
},
{
"id": "500004",
"last_scanned": "2025-04-17T20:44:27.861365+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500005",
"last_scanned": "2025-04-17T20:44:28.469193+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500001",
"last_scanned": "2025-04-17T20:44:28.957248+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500012",
"last_scanned": "2025-04-17T20:44:29.469038+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500011",
"last_scanned": "2025-04-17T20:44:34.681370+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "500002",
"last_scanned": "2025-04-17T20:44:35.459190+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "730494",
"last_scanned": "2025-04-17T21:05:17.116111+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "732981",
"last_scanned": "2025-04-17T20:59:18.951921+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "740351",
"last_scanned": "2025-04-17T21:05:43.742718+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "739763",
"last_scanned": "2025-04-17T21:01:17.839352+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "732159",
"last_scanned": "2025-04-17T21:01:32.957734+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "723430",
"last_scanned": "2025-04-17T21:04:13.084969+00:00",
"device_id": "a3919ba3219a48fe522181e5d2bab2b9"
},
{
"id": "88043A98",
"last_scanned": "2025-04-28T17:07:14.582487+00:00",
"device_id": "3924458c8a1bec286d9eabdb1bea7bb5"
},
{
"id": "^J1B983A04",
"last_scanned": "2025-04-29T19:51:53.295298+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:53.395591+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:53.593403+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:53.847634+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:54.117388+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:54.389359+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:54.633439+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:54.883523+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:55.207420+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:55.544369+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:55.804198+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:56.185549+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:56.566481+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:57.139310+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:57.962592+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
},
{
"id": "^J^J^J^J^J^J^J^J^J^J^J^J^J^J^J^J1B983A04",
"last_scanned": "2025-04-29T19:51:58.863292+00:00",
"device_id": "82395c211fed9b358b604c33ca0bea9e"
} }
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
["1.21.2","2.1.1","3.0.26","4.0.21","2.1.2","2.7.33","2.9.42","1.17.3","1.16.9","2.2.6","1.19.2","2.0.0","1.13.4","2.2.9","1.14.11","5.0.34","5.0.33","1.20.1","4.1.53","3.0.22","1.13.7","1.15.0","1.20.2","1.21.3","1.17.4","2.8.28","2.6.17","1.17.1","1.21.0","1.17.2","1.21.5","2.10.10","1.18.1","1.18.0","1.21.4","2.7.34","1.20.0","1.19.1","1.20.3","1.21.6","2.8.35","2.0.1","1.19.0","2.2.2","2.10.11","2.7.18","2.11.21","2.2.11","4.0.33"] ["2.11.21","2.10.10","1.13.7","1.18.1","5.3.45","2.2.11","1.21.2","1.21.3","1.19.0","5.1.85","4.0.21","5.1.78","3.0.22","2.10.11","5.2.46","1.13.4","1.17.3","2.1.2","2.2.6","1.20.0","5.0.45","5.0.34","5.0.33","1.15.0","1.20.3","1.17.4","1.19.2","1.16.9","1.21.0","1.21.6","2.2.2","1.20.1","2.7.18","2.9.42","2.0.1","2.8.28","1.17.2","1.21.5","1.20.2","5.2.62","4.0.33","1.19.1","2.1.1","2.6.17","4.1.53","2.2.9","5.3.41","1.18.0","5.0.51","5.0.47","2.0.0","5.1.57","1.17.1","2.7.33","2.7.34","5.1.87","1.21.4","3.0.26","5.2.42","5.3.38","1.14.11","5.2.49","5.2.61","2.8.35"]

View File

@ -2,7 +2,7 @@
"sessions": { "sessions": {
"9b5ac4acac7240a8d95decfdf283d3d1f3a0c9191d156693302952edbc86dfc5": { "9b5ac4acac7240a8d95decfdf283d3d1f3a0c9191d156693302952edbc86dfc5": {
"metadata": { "metadata": {
"expires": "Thu, 03 Oct 2024 20:45:52 GMT", "expires": "Fri, 09 May 2025 22:10:33 GMT",
"path": "/", "path": "/",
"comment": "", "comment": "",
"domain": "172.22.114.176", "domain": "172.22.114.176",
@ -14,8 +14,8 @@
"partitioned": true "partitioned": true
}, },
"cookiename": "TOKEN", "cookiename": "TOKEN",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI0NTJkOTgxOS0zZTVkLTQ4MzItYTVjMi1lNjA0Zjg5MTA1MmQiLCJwYXNzd29yZFJldmlzaW9uIjoxNzEzMTMxMjM1LCJpc1JlbWVtYmVyZWQiOnRydWUsImNzcmZUb2tlbiI6IjYxMDVkZDVmLWYzMWQtNDg5MS1iZGUxLWNkNjNmODAzNTZmZiIsImlhdCI6MTcyNTM5NjM1MiwiZXhwIjoxNzI3OTg4MzUyLCJqdGkiOiIxZDQ5Mjg3Yi1lNjdmLTRmYjEtYTEyOC05Mjg4MjZkYTE2MzIifQ._g2siuroWLCON2OPqI7Px9TDblt00NRK12XJXEIlpqk", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI0NTJkOTgxOS0zZTVkLTQ4MzItYTVjMi1lNjA0Zjg5MTA1MmQiLCJwYXNzd29yZFJldmlzaW9uIjoxNzMzODYxMzA0LCJpc1JlbWVtYmVyZWQiOnRydWUsImNzcmZUb2tlbiI6IjBlNGVmZWViLWE3Y2EtNGVjZi05NzQwLTk5NWExOTJjYzE0YyIsImlhdCI6MTc0NDIzNjYzMywiZXhwIjoxNzQ2ODI4NjMzLCJqdGkiOiJjM2VhNWYzMS0wMmQzLTQ0ZWMtODBlNC02ZWNkYTg0NWU2ZWYifQ.5zMbCTx0ll708zQkJKVmFX_lqKZi3m7xMNqQLQVdO14",
"csrf": "6105dd5f-f31d-4891-bde1-cd63f80356ff" "csrf": "0e4efeeb-a7ca-4ecf-9740-995a192cc14c"
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +1,57 @@
""" """HACS gives you a powerful UI to handle downloads of all your custom needs.
HACS gives you a powerful UI to handle downloads of all your custom needs.
For more details about this integration, please refer to the documentation at For more details about this integration, please refer to the documentation at
https://hacs.xyz/ https://hacs.xyz/
""" """
from __future__ import annotations
import os from __future__ import annotations
from typing import Any
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from homeassistant.components.frontend import async_remove_panel
from homeassistant.components.lovelace.system_health import system_health_info from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.start import async_at_start from homeassistant.helpers.start import async_at_start
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase from .base import HacsBase
from .const import DOMAIN, MINIMUM_HA_VERSION, STARTUP from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION, STARTUP
from .data_client import HacsDataClient from .data_client import HacsDataClient
from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode from .enums import HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend from .frontend import async_register_frontend
from .utils.configuration_schema import hacs_config_combined
from .utils.data import HacsData from .utils.data import HacsData
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands from .websocket import async_register_websocket_commands
CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA) PLATFORMS = [Platform.SWITCH, Platform.UPDATE]
async def async_initialize_integration( async def _async_initialize_integration(
hass: HomeAssistant, hass: HomeAssistant,
*, config_entry: ConfigEntry,
config_entry: ConfigEntry | None = None,
config: dict[str, Any] | None = None,
) -> bool: ) -> bool:
"""Initialize the integration""" """Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase() hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs() hacs.enable_hacs()
if config is not None:
if DOMAIN not in config:
return True
if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY:
return True
hacs.configuration.update_from_dict(
{
"config_type": ConfigurationType.YAML,
**config[DOMAIN],
"config": config[DOMAIN],
}
)
if config_entry is not None:
if config_entry.source == SOURCE_IMPORT: if config_entry.source == SOURCE_IMPORT:
# Import is not supported
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False return False
hacs.configuration.update_from_dict( hacs.configuration.update_from_dict(
{ {
"config_entry": config_entry, "config_entry": config_entry,
"config_type": ConfigurationType.CONFIG_ENTRY,
**config_entry.data, **config_entry.data,
**config_entry.options, **config_entry.options,
} },
) )
integration = await async_get_integration(hass, DOMAIN) integration = await async_get_integration(hass, DOMAIN)
@ -104,7 +82,6 @@ async def async_initialize_integration(
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode # If this happens, the users YAML is not valid, we assume YAML mode
pass pass
hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
hacs.core.config_path = hacs.hass.config.path() hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None: if hacs.core.ha_version is None:
@ -131,15 +108,14 @@ async def async_initialize_integration(
"""HACS startup tasks.""" """HACS startup tasks."""
hacs.enable_hacs() hacs.enable_hacs()
for location in ( try:
hass.config.path("custom_components/custom_updater.py"), import custom_components.custom_updater
hass.config.path("custom_components/custom_updater/__init__.py"), except ImportError:
): pass
if os.path.exists(location): else:
hacs.log.critical( hacs.log.critical(
"This cannot be used with custom_updater. " "HACS cannot be used with custom_updater. "
"To use this you need to remove custom_updater form %s", "To use HACS you need to remove custom_updater from `custom_components`",
location,
) )
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
@ -160,39 +136,23 @@ async def async_initialize_integration(
hacs.disable_hacs(HacsDisabledReason.RESTORE) hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False return False
if not hacs.configuration.experimental:
can_update = await hacs.async_can_update()
hacs.log.debug("Can update %s repositories", can_update)
hacs.set_active_categories() hacs.set_active_categories()
async_register_websocket_commands(hass) async_register_websocket_commands(hass)
async_register_frontend(hass, hacs) await async_register_frontend(hass, hacs)
if hacs.configuration.config_type == ConfigurationType.YAML: await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hass.async_create_task(
async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config)
)
hacs.log.info("Update entities are only supported when using UI configuration")
else:
await hass.config_entries.async_forward_entry_setups(
config_entry,
[Platform.SENSOR, Platform.UPDATE]
if hacs.configuration.experimental
else [Platform.SENSOR],
)
hacs.set_stage(HacsStage.SETUP) hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled: if hacs.system.disabled:
return False return False
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
hacs.set_stage(HacsStage.WAITING) hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts") hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
return not hacs.system.disabled return not hacs.system.disabled
async def async_try_startup(_=None): async def async_try_startup(_=None):
@ -202,10 +162,7 @@ async def async_initialize_integration(
except AIOGitHubAPIException: except AIOGitHubAPIException:
startup_result = False startup_result = False
if not startup_result: if not startup_result:
if ( if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
hacs.configuration.config_type == ConfigurationType.YAML
or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
):
hacs.log.info("Could not setup HACS, trying again in 15 min") hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup) async_call_later(hass, 900, async_try_startup)
return return
@ -213,37 +170,19 @@ async def async_initialize_integration(
await async_try_startup() await async_try_startup()
# Remove old (v0-v1) sensor if it exists, can be removed in v3
er = async_get_entity_registry(hass)
if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID):
er.async_remove(old_sensor)
# Mischief managed! # Mischief managed!
return True return True
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up this integration using yaml."""
if DOMAIN in config:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_configuration",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_configuration",
learn_more_url="https://hacs.xyz/docs/configuration/options",
)
LOGGER.warning(
"YAML configuration of HACS is deprecated and will be "
"removed in version 2.0.0, there will be no automatic "
"import of this. "
"Please remove it from your configuration, "
"restart Home Assistant and use the UI to configure it instead."
)
return await async_initialize_integration(hass=hass, config=config)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI.""" """Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
setup_result = await async_initialize_integration(hass=hass, config_entry=config_entry) setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry)
hacs: HacsBase = hass.data[DOMAIN] hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled return setup_result and not hacs.system.disabled
@ -259,7 +198,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
# Clear out pending queue # Clear out pending queue
hacs.queue.clear() hacs.queue.clear()
for task in hacs.recuring_tasks: for task in hacs.recurring_tasks:
# Cancel all pending tasks # Cancel all pending tasks
task() task()
@ -269,15 +208,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
try: try:
if hass.data.get("frontend_panels", {}).get("hacs"): if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel") hacs.log.info("Removing sidepanel")
hass.components.frontend.async_remove_panel("hacs") async_remove_panel(hass, "hacs")
except AttributeError: except AttributeError:
pass pass
platforms = ["sensor"] unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
if hacs.configuration.experimental:
platforms.append("update")
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
hacs.set_stage(None) hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED) hacs.disable_hacs(HacsDisabledReason.REMOVED)

View File

@ -1,16 +1,17 @@
"""Base HACS class.""" """Base HACS class."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import timedelta from datetime import timedelta
import gzip import gzip
import logging
import math import math
import os import os
import pathlib import pathlib
import shutil import shutil
from typing import TYPE_CHECKING, Any, Awaitable, Callable from typing import TYPE_CHECKING, Any
from aiogithubapi import ( from aiogithubapi import (
AIOGitHubAPIException, AIOGitHubAPIException,
@ -24,23 +25,22 @@ from aiogithubapi import (
from aiogithubapi.objects.repository import AIOGitHubAPIRepository from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession, ClientTimeout from aiohttp.client import ClientSession, ClientTimeout
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.components.persistent_notification import (
async_create as async_create_persistent_notification,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.loader import Integration from homeassistant.loader import Integration
from homeassistant.util import dt from homeassistant.util import dt
from custom_components.hacs.repositories.base import (
HACS_MANIFEST_KEYS_TO_EXPORT,
REPOSITORY_KEYS_TO_EXPORT,
)
from .const import DOMAIN, TV, URL_BASE from .const import DOMAIN, TV, URL_BASE
from .coordinator import HacsUpdateCoordinator
from .data_client import HacsDataClient from .data_client import HacsDataClient
from .enums import ( from .enums import (
ConfigurationType,
HacsCategory, HacsCategory,
HacsDisabledReason, HacsDisabledReason,
HacsDispatchEvent, HacsDispatchEvent,
@ -58,12 +58,14 @@ from .exceptions import (
HacsRepositoryExistException, HacsRepositoryExistException,
HomeAssistantCoreRepositoryException, HomeAssistantCoreRepositoryException,
) )
from .repositories import RERPOSITORY_CLASSES from .repositories import REPOSITORY_CLASSES
from .utils.decode import decode_content from .repositories.base import HACS_MANIFEST_KEYS_TO_EXPORT, REPOSITORY_KEYS_TO_EXPORT
from .utils.file_system import async_exists
from .utils.json import json_loads from .utils.json import json_loads
from .utils.logger import LOGGER from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager from .utils.queue_manager import QueueManager
from .utils.store import async_load_from_store, async_save_to_store from .utils.store import async_load_from_store, async_save_to_store
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING: if TYPE_CHECKING:
from .repositories.base import HacsRepository from .repositories.base import HacsRepository
@ -113,15 +115,11 @@ class HacsConfiguration:
appdaemon: bool = False appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict) config: dict[str, Any] = field(default_factory=dict)
config_entry: ConfigEntry | None = None config_entry: ConfigEntry | None = None
config_type: ConfigurationType | None = None
country: str = "ALL" country: str = "ALL"
debug: bool = False debug: bool = False
dev: bool = False dev: bool = False
experimental: bool = False
frontend_repo_url: str = "" frontend_repo_url: str = ""
frontend_repo: str = "" frontend_repo: str = ""
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
plugin_path: str = "www/community/" plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/" python_script_path: str = "python_scripts/"
python_script: bool = False python_script: bool = False
@ -142,6 +140,8 @@ class HacsConfiguration:
raise HacsException("Configuration is not valid.") raise HacsException("Configuration is not valid.")
for key in data: for key in data:
if key in {"experimental", "netdaemon", "release_limit", "debug"}:
continue
self.__setattr__(key, data[key]) self.__setattr__(key, data[key])
@ -217,6 +217,13 @@ class HacsRepositories:
"""Return a list of downloaded repositories.""" """Return a list of downloaded repositories."""
return [repo for repo in self._repositories if repo.data.installed] return [repo for repo in self._repositories if repo.data.installed]
def category_downloaded(self, category: HacsCategory) -> bool:
"""Check if a given category has been downloaded."""
for repository in self.list_downloaded:
if repository.data.category == category:
return True
return False
def register(self, repository: HacsRepository, default: bool = False) -> None: def register(self, repository: HacsRepository, default: bool = False) -> None:
"""Register a repository.""" """Register a repository."""
repo_id = str(repository.data.id) repo_id = str(repository.data.id)
@ -348,9 +355,6 @@ class HacsRepositories:
class HacsBase: class HacsBase:
"""Base HACS class.""" """Base HACS class."""
common = HacsCommon()
configuration = HacsConfiguration()
core = HacsCore()
data: HacsData | None = None data: HacsData | None = None
data_client: HacsDataClient | None = None data_client: HacsDataClient | None = None
frontend_version: str | None = None frontend_version: str | None = None
@ -358,17 +362,24 @@ class HacsBase:
githubapi: GitHubAPI | None = None githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None hass: HomeAssistant | None = None
integration: Integration | None = None integration: Integration | None = None
log: logging.Logger = LOGGER
queue: QueueManager | None = None queue: QueueManager | None = None
recuring_tasks = []
repositories: HacsRepositories = HacsRepositories()
repository: AIOGitHubAPIRepository | None = None repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None session: ClientSession | None = None
stage: HacsStage | None = None stage: HacsStage | None = None
status = HacsStatus()
system = HacsSystem()
validation: ValidationManager | None = None validation: ValidationManager | None = None
version: str | None = None version: AwesomeVersion | None = None
def __init__(self) -> None:
"""Initialize."""
self.common = HacsCommon()
self.configuration = HacsConfiguration()
self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {}
self.core = HacsCore()
self.log = LOGGER
self.recurring_tasks: list[Callable[[], None]] = []
self.repositories = HacsRepositories()
self.status = HacsStatus()
self.system = HacsSystem()
@property @property
def integration_dir(self) -> pathlib.Path: def integration_dir(self) -> pathlib.Path:
@ -394,12 +405,7 @@ class HacsBase:
if reason != HacsDisabledReason.REMOVED: if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason) self.log.error("HACS is disabled - %s", reason)
if ( if reason == HacsDisabledReason.INVALID_TOKEN:
reason == HacsDisabledReason.INVALID_TOKEN
and self.configuration.config_type == ConfigurationType.CONFIG_ENTRY
):
self.configuration.config_entry.state = ConfigEntryState.SETUP_ERROR
self.configuration.config_entry.reason = "Authentication failed"
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass) self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
def enable_hacs(self) -> None: def enable_hacs(self) -> None:
@ -413,12 +419,14 @@ class HacsBase:
if category not in self.common.categories: if category not in self.common.categories:
self.log.info("Enable category: %s", category) self.log.info("Enable category: %s", category)
self.common.categories.add(category) self.common.categories.add(category)
self.coordinators[category] = HacsUpdateCoordinator()
def disable_hacs_category(self, category: HacsCategory) -> None: def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category.""" """Disable HACS category."""
if category in self.common.categories: if category in self.common.categories:
self.log.info("Disabling category: %s", category) self.log.info("Disabling category: %s", category)
self.common.categories.pop(category) self.common.categories.pop(category)
self.coordinators.pop(category)
async def async_save_file(self, file_path: str, content: Any) -> bool: async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file.""" """Save a file."""
@ -451,12 +459,13 @@ class HacsBase:
try: try:
await self.hass.async_add_executor_job(_write_file) await self.hass.async_add_executor_job(_write_file)
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as error: ) as error:
self.log.error("Could not write data to %s - %s", file_path, error) self.log.error("Could not write data to %s - %s", file_path, error)
return False return False
return os.path.exists(file_path) return await async_exists(self.hass, file_path)
async def async_can_update(self) -> int: async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for.""" """Helper to calculate the number of repositories we can fetch data for."""
@ -472,24 +481,13 @@ class HacsBase:
) )
self.disable_hacs(HacsDisabledReason.RATE_LIMIT) self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.log.exception(exception) self.log.exception(exception)
return 0 return 0
async def async_github_get_hacs_default_file(self, filename: str) -> list:
"""Get the content of a default file."""
response = await self.async_github_api_method(
method=self.githubapi.repos.contents.get,
repository=HacsGitHubRepo.DEFAULT,
path=filename,
)
if response is None:
return []
return json_loads(decode_content(response.data.content))
async def async_github_api_method( async def async_github_api_method(
self, self,
method: Callable[[], Awaitable[TV]], method: Callable[[], Awaitable[TV]],
@ -513,7 +511,8 @@ class HacsBase:
except GitHubException as exception: except GitHubException as exception:
_exception = exception _exception = exception
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.log.exception(exception) self.log.exception(exception)
_exception = exception _exception = exception
@ -545,7 +544,7 @@ class HacsBase:
): ):
raise AddonRepositoryException() raise AddonRepositoryException()
if category not in RERPOSITORY_CLASSES: if category not in REPOSITORY_CLASSES:
self.log.warning( self.log.warning(
"%s is not a valid repository category, %s will not be registered.", "%s is not a valid repository category, %s will not be registered.",
category, category,
@ -556,7 +555,7 @@ class HacsBase:
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None: if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
repository_full_name = renamed repository_full_name = renamed
repository: HacsRepository = RERPOSITORY_CLASSES[category](self, repository_full_name) repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name)
if check: if check:
try: try:
await repository.async_registration(ref) await repository.async_registration(ref)
@ -566,7 +565,8 @@ class HacsBase:
self.log.error("Validation for %s failed.", repository_full_name) self.log.error("Validation for %s failed.", repository_full_name)
if self.system.action: if self.system.action:
raise HacsException( raise HacsException(
f"::error:: Validation for {repository_full_name} failed." f"::error:: Validation for {
repository_full_name} failed."
) )
return repository.validate.errors return repository.validate.errors
if self.system.action: if self.system.action:
@ -582,7 +582,8 @@ class HacsBase:
except AIOGitHubAPIException as exception: except AIOGitHubAPIException as exception:
self.common.skip.add(repository.data.full_name) self.common.skip.add(repository.data.full_name)
raise HacsException( raise HacsException(
f"Validation for {repository_full_name} failed with {exception}." f"Validation for {
repository_full_name} failed with {exception}."
) from exception ) from exception
if self.status.new: if self.status.new:
@ -592,7 +593,7 @@ class HacsBase:
repository.data.id = repository_id repository.data.id = repository_id
else: else:
if self.hass is not None and ((check and repository.data.new) or self.status.new): if self.hass is not None and check and repository.data.new:
self.async_dispatch( self.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,
{ {
@ -613,91 +614,84 @@ class HacsBase:
for repo in critical: for repo in critical:
if not repo["acknowledged"]: if not repo["acknowledged"]:
self.log.critical("URGENT!: Check the HACS panel!") self.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create( async_create_persistent_notification(
title="URGENT!", message="**Check the HACS panel!**" self.hass, title="URGENT!", message="**Check the HACS panel!**"
) )
break break
if not self.configuration.experimental: self.recurring_tasks.append(
self.recuring_tasks.append( async_track_time_interval(
self.hass.helpers.event.async_track_time_interval( self.hass,
self.async_update_downloaded_repositories, timedelta(hours=48)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_all_repositories,
timedelta(hours=96),
)
)
else:
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_load_hacs_from_github, self.async_load_hacs_from_github,
timedelta(hours=48), timedelta(hours=48),
) )
) )
self.recuring_tasks.append( self.recurring_tasks.append(
self.hass.helpers.event.async_track_time_interval( async_track_time_interval(
self.async_update_downloaded_custom_repositories, timedelta(hours=48) self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48)
) )
) )
self.recuring_tasks.append( self.recurring_tasks.append(
self.hass.helpers.event.async_track_time_interval( async_track_time_interval(
self.async_get_all_category_repositories, timedelta(hours=6) self.hass, self.async_get_all_category_repositories, timedelta(hours=6)
) )
) )
self.recuring_tasks.append( self.recurring_tasks.append(
self.hass.helpers.event.async_track_time_interval( async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5))
self.async_check_rate_limit, timedelta(minutes=5)
) )
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_process_queue, timedelta(minutes=10))
) )
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval( self.recurring_tasks.append(
self.async_prosess_queue, timedelta(minutes=10) async_track_time_interval(
self.hass, self.async_handle_critical_repositories, timedelta(hours=6)
) )
) )
self.recuring_tasks.append( unsub = self.hass.bus.async_listen_once(
self.hass.helpers.event.async_track_time_interval(
self.async_handle_critical_repositories, timedelta(hours=6)
)
)
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write
) )
if config_entry := self.configuration.config_entry:
config_entry.async_on_unload(unsub)
self.log.debug("There are %s scheduled recurring tasks", len(self.recuring_tasks)) self.log.debug("There are %s scheduled recurring tasks", len(self.recurring_tasks))
self.status.startup = False self.status.startup = False
self.async_dispatch(HacsDispatchEvent.STATUS, {}) self.async_dispatch(HacsDispatchEvent.STATUS, {})
await self.async_handle_removed_repositories() await self.async_handle_removed_repositories()
await self.async_get_all_category_repositories() await self.async_get_all_category_repositories()
await self.async_update_downloaded_repositories()
self.set_stage(HacsStage.RUNNING) self.set_stage(HacsStage.RUNNING)
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True}) self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await self.async_handle_critical_repositories() await self.async_handle_critical_repositories()
await self.async_prosess_queue() await self.async_process_queue()
self.async_dispatch(HacsDispatchEvent.STATUS, {}) self.async_dispatch(HacsDispatchEvent.STATUS, {})
async def async_download_file(self, url: str, *, headers: dict | None = None) -> bytes | None: async def async_download_file(
self,
url: str,
*,
headers: dict | None = None,
keep_url: bool = False,
nolog: bool = False,
**_,
) -> bytes | None:
"""Download files, and return the content.""" """Download files, and return the content."""
if url is None: if url is None:
return None return None
if "tags/" in url: if not keep_url and "tags/" in url:
url = url.replace("tags/", "") url = url.replace("tags/", "")
self.log.debug("Downloading %s", url) self.log.debug("Trying to download %s", url)
timeouts = 0 timeouts = 0
while timeouts < 5: while timeouts < 5:
@ -713,9 +707,10 @@ class HacsBase:
return await request.read() return await request.read()
raise HacsException( raise HacsException(
f"Got status code {request.status} when trying to download {url}" f"Got status code {
request.status} when trying to download {url}"
) )
except asyncio.TimeoutError: except TimeoutError:
self.log.warning( self.log.warning(
"A timeout of 60! seconds was encountered while downloading %s, " "A timeout of 60! seconds was encountered while downloading %s, "
"using over 60 seconds to download a single file is not normal. " "using over 60 seconds to download a single file is not normal. "
@ -731,19 +726,30 @@ class HacsBase:
continue continue
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
if not nolog:
self.log.exception("Download failed - %s", exception) self.log.exception("Download failed - %s", exception)
return None return None
async def async_recreate_entities(self) -> None: async def async_recreate_entities(self) -> None:
"""Recreate entities.""" """Recreate entities."""
if self.configuration == ConfigurationType.YAML or not self.configuration.experimental: platforms = [Platform.UPDATE]
return
platforms = [Platform.SENSOR, Platform.UPDATE]
# Workaround for core versions without https://github.com/home-assistant/core/pull/117084
if self.core.ha_version < AwesomeVersion("2024.6.0"):
unload_platforms_lock = asyncio.Lock()
async with unload_platforms_lock:
on_unload = self.configuration.config_entry._on_unload
self.configuration.config_entry._on_unload = []
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
self.configuration.config_entry._on_unload = on_unload
else:
await self.hass.config_entries.async_unload_platforms( await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry, entry=self.configuration.config_entry,
platforms=platforms, platforms=platforms,
@ -760,49 +766,40 @@ class HacsBase:
def set_active_categories(self) -> None: def set_active_categories(self) -> None:
"""Set the active categories.""" """Set the active categories."""
self.common.categories = set() self.common.categories = set()
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN): for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN, HacsCategory.TEMPLATE):
self.enable_hacs_category(HacsCategory(category)) self.enable_hacs_category(HacsCategory(category))
if self.configuration.experimental and self.core.ha_version >= "2023.4.0b0": if (
self.enable_hacs_category(HacsCategory.TEMPLATE) HacsCategory.PYTHON_SCRIPT in self.hass.config.components
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
if HacsCategory.PYTHON_SCRIPT in self.hass.config.components: ):
self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT) self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
if self.hass.services.has_service("frontend", "reload_themes"): if self.hass.services.has_service(
"frontend", "reload_themes"
) or self.repositories.category_downloaded(HacsCategory.THEME):
self.enable_hacs_category(HacsCategory.THEME) self.enable_hacs_category(HacsCategory.THEME)
if self.configuration.appdaemon: if self.configuration.appdaemon:
self.enable_hacs_category(HacsCategory.APPDAEMON) self.enable_hacs_category(HacsCategory.APPDAEMON)
if self.configuration.netdaemon:
downloaded_netdaemon = [
x
for x in self.repositories.list_downloaded
if x.data.category == HacsCategory.NETDAEMON
]
if len(downloaded_netdaemon) != 0:
self.log.warning(
"NetDaemon in HACS is deprectaded. It will stop working in the future. "
"Please remove all your current NetDaemon repositories from HACS "
"and download them manually if you want to continue using them."
)
self.enable_hacs_category(HacsCategory.NETDAEMON)
async def async_load_hacs_from_github(self, _=None) -> None: async def async_load_hacs_from_github(self, _=None) -> None:
"""Load HACS from GitHub.""" """Load HACS from GitHub."""
if self.configuration.experimental and self.status.inital_fetch_done: if self.status.inital_fetch_done:
return return
try: try:
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION) repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
should_recreate_entities = False
if repository is None: if repository is None:
should_recreate_entities = True
await self.async_register_repository( await self.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION, repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION, category=HacsCategory.INTEGRATION,
default=True, default=True,
) )
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION) repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
elif self.configuration.experimental and not self.status.startup: elif not self.status.startup:
self.log.error("Scheduling update of hacs/integration") self.log.error("Scheduling update of hacs/integration")
self.queue.add(repository.common_update()) self.queue.add(repository.common_update())
if repository is None: if repository is None:
@ -813,6 +810,9 @@ class HacsBase:
repository.data.new = False repository.data.new = False
repository.data.releases = True repository.data.releases = True
if should_recreate_entities:
await self.async_recreate_entities()
self.repository = repository.repository_object self.repository = repository.repository_object
self.repositories.mark_default(repository) self.repositories.mark_default(repository)
except HacsException as exception: except HacsException as exception:
@ -832,8 +832,6 @@ class HacsBase:
await asyncio.gather( await asyncio.gather(
*[ *[
self.async_get_category_repositories_experimental(category) self.async_get_category_repositories_experimental(category)
if self.configuration.experimental
else self.async_get_category_repositories(HacsCategory(category))
for category in self.common.categories or [] for category in self.common.categories or []
] ]
) )
@ -842,7 +840,7 @@ class HacsBase:
"""Update all category repositories.""" """Update all category repositories."""
self.log.debug("Fetching updated content for %s", category) self.log.debug("Fetching updated content for %s", category)
try: try:
category_data = await self.data_client.get_data(category) category_data = await self.data_client.get_data(category, validate=True)
except HacsNotModifiedException: except HacsNotModifiedException:
self.log.debug("No updates for %s", category) self.log.debug("No updates for %s", category)
return return
@ -853,14 +851,14 @@ class HacsBase:
await self.data.register_unknown_repositories(category_data, category) await self.data.register_unknown_repositories(category_data, category)
for repo_id, repo_data in category_data.items(): for repo_id, repo_data in category_data.items():
repo = repo_data["full_name"] repo_name = repo_data["full_name"]
if self.common.renamed_repositories.get(repo): if self.common.renamed_repositories.get(repo_name):
repo = self.common.renamed_repositories[repo] repo_name = self.common.renamed_repositories[repo_name]
if self.repositories.is_removed(repo): if self.repositories.is_removed(repo_name):
continue continue
if repo in self.common.archived_repositories: if repo_name in self.common.archived_repositories:
continue continue
if repository := self.repositories.get_by_full_name(repo): if repository := self.repositories.get_by_full_name(repo_name):
self.repositories.set_repository_id(repository, repo_id) self.repositories.set_repository_id(repository, repo_id)
self.repositories.mark_default(repository) self.repositories.mark_default(repository)
if repository.data.last_fetched is None or ( if repository.data.last_fetched is None or (
@ -871,15 +869,6 @@ class HacsBase:
repository.repository_manifest.update_data( repository.repository_manifest.update_data(
{**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest} {**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest}
) )
self.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"id": 1337,
"action": "update",
"repository": repository.data.full_name,
"repository_id": repository.data.id,
},
)
if category == "integration": if category == "integration":
self.status.inital_fetch_done = True self.status.inital_fetch_done = True
@ -896,50 +885,8 @@ class HacsBase:
) )
self.repositories.unregister(repository) self.repositories.unregister(repository)
async def async_get_category_repositories(self, category: HacsCategory) -> None: self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
"""Get repositories from category.""" self.coordinators[category].async_update_listeners()
if self.system.disabled:
return
try:
repositories = await self.async_github_get_hacs_default_file(category)
except HacsException:
return
for repo in repositories:
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
continue
if repo in self.common.archived_repositories:
continue
repository = self.repositories.get_by_full_name(repo)
if repository is not None:
self.repositories.mark_default(repository)
if self.status.new and self.configuration.dev:
# Force update for new installations
self.queue.add(repository.common_update())
continue
self.queue.add(
self.async_register_repository(
repository_full_name=repo,
category=category,
default=True,
)
)
async def async_update_all_repositories(self, _=None) -> None:
"""Update all repositories."""
if self.system.disabled:
return
self.log.debug("Starting recurring background task for all repositories")
for repository in self.repositories.list_all:
if repository.data.category in self.common.categories:
self.queue.add(repository.common_update())
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {"action": "reload"})
self.log.debug("Recurring background task for all repositories done")
async def async_check_rate_limit(self, _=None) -> None: async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit.""" """Check rate limit."""
@ -951,9 +898,9 @@ class HacsBase:
self.log.debug("Ratelimit indicate we can update %s", can_update) self.log.debug("Ratelimit indicate we can update %s", can_update)
if can_update > 0: if can_update > 0:
self.enable_hacs() self.enable_hacs()
await self.async_prosess_queue() await self.async_process_queue()
async def async_prosess_queue(self, _=None) -> None: async def async_process_queue(self, _=None) -> None:
"""Process the queue.""" """Process the queue."""
if self.system.disabled: if self.system.disabled:
self.log.debug("HACS is disabled") self.log.debug("HACS is disabled")
@ -993,12 +940,7 @@ class HacsBase:
self.log.info("Loading removed repositories") self.log.info("Loading removed repositories")
try: try:
if self.configuration.experimental: removed_repositories = await self.data_client.get_data("removed", validate=True)
removed_repositories = await self.data_client.get_data("removed")
else:
removed_repositories = await self.async_github_get_hacs_default_file(
HacsCategory.REMOVED
)
except HacsException: except HacsException:
return return
@ -1013,7 +955,6 @@ class HacsBase:
continue continue
if repository.data.installed: if repository.data.installed:
if removed.removal_type != "critical": if removed.removal_type != "critical":
if self.configuration.experimental:
async_create_issue( async_create_issue(
hass=self.hass, hass=self.hass,
domain=DOMAIN, domain=DOMAIN,
@ -1042,30 +983,43 @@ class HacsBase:
if need_to_save: if need_to_save:
await self.data.async_write() await self.data.async_write()
async def async_update_downloaded_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled or self.configuration.experimental:
return
self.log.info("Starting recurring background task for downloaded repositories")
for repository in self.repositories.list_downloaded:
if repository.data.category in self.common.categories:
self.queue.add(repository.update_repository(ignore_issues=True))
self.log.debug("Recurring background task for downloaded repositories done")
async def async_update_downloaded_custom_repositories(self, _=None) -> None: async def async_update_downloaded_custom_repositories(self, _=None) -> None:
"""Execute the task.""" """Execute the task."""
if self.system.disabled or not self.configuration.experimental: if self.system.disabled:
return return
self.log.info("Starting recurring background task for downloaded custom repositories") self.log.info("Starting recurring background task for downloaded custom repositories")
repositories_to_update = 0
repositories_updated = asyncio.Event()
async def update_repository(repository: HacsRepository) -> None:
"""Update a repository"""
nonlocal repositories_to_update
await repository.update_repository(ignore_issues=True)
repositories_to_update -= 1
if not repositories_to_update:
repositories_updated.set()
for repository in self.repositories.list_downloaded: for repository in self.repositories.list_downloaded:
if ( if (
repository.data.category in self.common.categories repository.data.category in self.common.categories
and not self.repositories.is_default(repository.data.id) and not self.repositories.is_default(repository.data.id)
): ):
self.queue.add(repository.update_repository(ignore_issues=True)) repositories_to_update += 1
self.queue.add(update_repository(repository))
async def update_coordinators() -> None:
"""Update all coordinators."""
await repositories_updated.wait()
for coordinator in self.coordinators.values():
coordinator.async_update_listeners()
if config_entry := self.configuration.config_entry:
config_entry.async_create_background_task(
self.hass, update_coordinators(), "update_coordinators"
)
else:
self.hass.async_create_background_task(update_coordinators(), "update_coordinators")
self.log.debug("Recurring background task for downloaded custom repositories done") self.log.debug("Recurring background task for downloaded custom repositories done")
@ -1077,10 +1031,7 @@ class HacsBase:
was_installed = False was_installed = False
try: try:
if self.configuration.experimental: critical = await self.data_client.get_data("critical", validate=True)
critical = await self.data_client.get_data("critical")
else:
critical = await self.async_github_get_hacs_default_file("critical")
except (GitHubNotModifiedException, HacsNotModifiedException): except (GitHubNotModifiedException, HacsNotModifiedException):
return return
except HacsException: except HacsException:
@ -1134,11 +1085,10 @@ class HacsBase:
self.log.critical("Restarting Home Assistant") self.log.critical("Restarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100)) self.hass.async_create_task(self.hass.async_stop(100))
@callback async def async_setup_frontend_endpoint_plugin(self) -> None:
def async_setup_frontend_endpoint_plugin(self) -> None:
"""Setup the http endpoints for plugins if its not already handled.""" """Setup the http endpoints for plugins if its not already handled."""
if self.status.active_frontend_endpoint_plugin or not os.path.exists( if self.status.active_frontend_endpoint_plugin or not await async_exists(
self.hass.config.path("www/community") self.hass, self.hass.config.path("www/community")
): ):
return return
@ -1150,26 +1100,11 @@ class HacsBase:
use_cache, use_cache,
) )
self.hass.http.register_static_path( await async_register_static_path(
self.hass,
URL_BASE, URL_BASE,
self.hass.config.path("www/community"), self.hass.config.path("www/community"),
cache_headers=use_cache, cache_headers=use_cache,
) )
self.status.active_frontend_endpoint_plugin = True self.status.active_frontend_endpoint_plugin = True
@callback
def async_setup_frontend_endpoint_themes(self) -> None:
"""Setup the http endpoints for themes if its not already handled."""
if (
self.configuration.experimental
or self.status.active_frontend_endpoint_theme
or not os.path.exists(self.hass.config.path("themes"))
):
return
self.log.info("Setting up themes endpoint")
# Register themes
self.hass.http.register_static_path(f"{URL_BASE}/themes", self.hass.config.path("themes"))
self.status.active_frontend_endpoint_theme = True

View File

@ -1,29 +1,32 @@
"""Adds config flow for HACS.""" """Adds config flow for HACS."""
from __future__ import annotations from __future__ import annotations
import asyncio
from contextlib import suppress
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from aiogithubapi import GitHubDeviceAPI, GitHubException from aiogithubapi import (
GitHubDeviceAPI,
GitHubException,
GitHubLoginDeviceModel,
GitHubLoginOauthModel,
)
from aiogithubapi.common.const import OAUTH_USER_LOGIN from aiogithubapi.common.const import OAUTH_USER_LOGIN
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow, OptionsFlow
from homeassistant.const import __version__ as HAVERSION from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_call_later
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
import voluptuous as vol import voluptuous as vol
from .base import HacsBase from .base import HacsBase
from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION
from .enums import ConfigurationType
from .utils.configuration_schema import ( from .utils.configuration_schema import (
APPDAEMON, APPDAEMON,
COUNTRY, COUNTRY,
DEBUG,
EXPERIMENTAL,
NETDAEMON,
RELEASE_LIMIT,
SIDEPANEL_ICON, SIDEPANEL_ICON,
SIDEPANEL_TITLE, SIDEPANEL_TITLE,
) )
@ -33,23 +36,22 @@ if TYPE_CHECKING:
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for HACS.""" """Config flow for HACS."""
hass: HomeAssistant
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self): hass: HomeAssistant
activation_task: asyncio.Task | None = None
device: GitHubDeviceAPI | None = None
_registration: GitHubLoginDeviceModel | None = None
_activation: GitHubLoginOauthModel | None = None
_reauth: bool = False
def __init__(self) -> None:
"""Initialize.""" """Initialize."""
self._errors = {} self._errors = {}
self.device = None
self.activation = None
self.log = LOGGER
self._progress_task = None
self._login_device = None
self._reauth = False
self._user_input = {} self._user_input = {}
async def async_step_user(self, user_input): async def async_step_user(self, user_input):
@ -69,49 +71,56 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_device(user_input) return await self.async_step_device(user_input)
## Initial form # Initial form
return await self._show_config_form(user_input) return await self._show_config_form(user_input)
async def async_step_device(self, _user_input): async def async_step_device(self, _user_input):
"""Handle device steps""" """Handle device steps."""
async def _wait_for_activation(_=None): async def _wait_for_activation() -> None:
if self._login_device is None or self._login_device.expires_in is None: try:
async_call_later(self.hass, 1, _wait_for_activation) response = await self.device.activation(device_code=self._registration.device_code)
return self._activation = response.data
finally:
response = await self.device.activation(device_code=self._login_device.device_code) async def _progress():
self.activation = response.data with suppress(UnknownFlow):
self.hass.async_create_task( await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
)
if not self.activation:
integration = await async_get_integration(self.hass, DOMAIN)
if not self.device: if not self.device:
integration = await async_get_integration(self.hass, DOMAIN)
self.device = GitHubDeviceAPI( self.device = GitHubDeviceAPI(
client_id=CLIENT_ID, client_id=CLIENT_ID,
session=aiohttp_client.async_get_clientsession(self.hass), session=aiohttp_client.async_get_clientsession(self.hass),
**{"client_name": f"HACS/{integration.version}"}, **{"client_name": f"HACS/{integration.version}"},
) )
async_call_later(self.hass, 1, _wait_for_activation)
try: try:
response = await self.device.register() response = await self.device.register()
self._login_device = response.data self._registration = response.data
return self.async_show_progress(
step_id="device",
progress_action="wait_for_device",
description_placeholders={
"url": OAUTH_USER_LOGIN,
"code": self._login_device.user_code,
},
)
except GitHubException as exception: except GitHubException as exception:
self.log.error(exception) LOGGER.exception(exception)
return self.async_abort(reason="github") return self.async_abort(reason="could_not_register")
if self.activation_task is None:
self.activation_task = self.hass.async_create_task(_wait_for_activation())
if self.activation_task.done():
if (exception := self.activation_task.exception()) is not None:
LOGGER.exception(exception)
return self.async_show_progress_done(next_step_id="could_not_register")
return self.async_show_progress_done(next_step_id="device_done") return self.async_show_progress_done(next_step_id="device_done")
show_progress_kwargs = {
"step_id": "device",
"progress_action": "wait_for_device",
"description_placeholders": {
"url": OAUTH_USER_LOGIN,
"code": self._registration.user_code,
},
"progress_task": self.activation_task,
}
return self.async_show_progress(**show_progress_kwargs)
async def _show_config_form(self, user_input): async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data.""" """Show the configuration form to edit location data."""
@ -133,9 +142,6 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"acc_untested", default=user_input.get("acc_untested", False) "acc_untested", default=user_input.get("acc_untested", False)
): bool, ): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool, vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
vol.Optional(
"experimental", default=user_input.get("experimental", False)
): bool,
} }
), ),
errors=self._errors, errors=self._errors,
@ -146,7 +152,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if self._reauth: if self._reauth:
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
existing_entry, data={**existing_entry.data, "token": self.activation.access_token} existing_entry, data={**existing_entry.data, "token": self._activation.access_token}
) )
await self.hass.config_entries.async_reload(existing_entry.entry_id) await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
@ -154,13 +160,17 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry( return self.async_create_entry(
title="", title="",
data={ data={
"token": self.activation.access_token, "token": self._activation.access_token,
}, },
options={ options={
"experimental": self._user_input.get("experimental", False), "experimental": True,
}, },
) )
async def async_step_could_not_register(self, _user_input=None):
"""Handle issues that need transition await from progress step."""
return self.async_abort(reason="could_not_register")
async def async_step_reauth(self, _user_input=None): async def async_step_reauth(self, _user_input=None):
"""Perform reauth upon an API authentication error.""" """Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
@ -181,11 +191,12 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return HacsOptionsFlowHandler(config_entry) return HacsOptionsFlowHandler(config_entry)
class HacsOptionsFlowHandler(config_entries.OptionsFlow): class HacsOptionsFlowHandler(OptionsFlow):
"""HACS config flow options handler.""" """HACS config flow options handler."""
def __init__(self, config_entry): def __init__(self, config_entry):
"""Initialize HACS options flow.""" """Initialize HACS options flow."""
if AwesomeVersion(HAVERSION) < "2024.11.99":
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, _user_input=None): async def async_step_init(self, _user_input=None):
@ -196,10 +207,7 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
hacs: HacsBase = self.hass.data.get(DOMAIN) hacs: HacsBase = self.hass.data.get(DOMAIN)
if user_input is not None: if user_input is not None:
limit = int(user_input.get(RELEASE_LIMIT, 5)) return self.async_create_entry(title="", data={**user_input, "experimental": True})
if limit <= 0 or limit > 100:
return self.async_abort(reason="release_limit_value")
return self.async_create_entry(title="", data=user_input)
if hacs is None or hacs.configuration is None: if hacs is None or hacs.configuration is None:
return self.async_abort(reason="not_setup") return self.async_abort(reason="not_setup")
@ -207,18 +215,11 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
if hacs.queue.has_pending_tasks: if hacs.queue.has_pending_tasks:
return self.async_abort(reason="pending_tasks") return self.async_abort(reason="pending_tasks")
if hacs.configuration.config_type == ConfigurationType.YAML:
schema = {vol.Optional("not_in_use", default=""): str}
else:
schema = { schema = {
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str, vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str, vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
vol.Optional(RELEASE_LIMIT, default=hacs.configuration.release_limit): int,
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE), vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool, vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
vol.Optional(NETDAEMON, default=hacs.configuration.netdaemon): bool,
vol.Optional(DEBUG, default=hacs.configuration.debug): bool,
vol.Optional(EXPERIMENTAL, default=hacs.configuration.experimental): bool,
} }
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))

View File

@ -1,4 +1,5 @@
"""Constants for HACS""" """Constants for HACS"""
from typing import TypeVar from typing import TypeVar
from aiogithubapi.common.const import ACCEPT_HEADERS from aiogithubapi.common.const import ACCEPT_HEADERS
@ -6,7 +7,7 @@ from aiogithubapi.common.const import ACCEPT_HEADERS
NAME_SHORT = "HACS" NAME_SHORT = "HACS"
DOMAIN = "hacs" DOMAIN = "hacs"
CLIENT_ID = "395a8e669c5de9f7c6e8" CLIENT_ID = "395a8e669c5de9f7c6e8"
MINIMUM_HA_VERSION = "2023.6.0" MINIMUM_HA_VERSION = "2024.4.1"
URL_BASE = "/hacsfiles" URL_BASE = "/hacsfiles"

View File

@ -1,12 +1,25 @@
"""HACS Data client.""" """HACS Data client."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import Any from typing import Any
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
import voluptuous as vol
from .exceptions import HacsException, HacsNotModifiedException from .exceptions import HacsException, HacsNotModifiedException
from .utils.logger import LOGGER
from .utils.validate import (
VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REPO_DATA,
)
CRITICAL_REMOVED_VALIDATORS = {
"critical": VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
"removed": VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
}
class HacsDataClient: class HacsDataClient:
@ -39,7 +52,7 @@ class HacsDataClient:
response.raise_for_status() response.raise_for_status()
except HacsNotModifiedException: except HacsNotModifiedException:
raise raise
except asyncio.TimeoutError: except TimeoutError:
raise HacsException("Timeout of 60s reached") from None raise HacsException("Timeout of 60s reached") from None
except Exception as exception: except Exception as exception:
raise HacsException(f"Error fetching data from HACS: {exception}") from exception raise HacsException(f"Error fetching data from HACS: {exception}") from exception
@ -48,9 +61,37 @@ class HacsDataClient:
return await response.json() return await response.json()
async def get_data(self, section: str | None) -> dict[str, dict[str, Any]]: async def get_data(self, section: str | None, *, validate: bool) -> dict[str, dict[str, Any]]:
"""Get data.""" """Get data."""
return await self._do_request(filename="data.json", section=section) data = await self._do_request(filename="data.json", section=section)
if not validate:
return data
if section in VALIDATE_FETCHED_V2_REPO_DATA:
validated = {}
for key, repo_data in data.items():
try:
validated[key] = VALIDATE_FETCHED_V2_REPO_DATA[section](repo_data)
except vol.Invalid as exception:
LOGGER.info(
"Got invalid data for %s (%s)", repo_data.get("full_name", key), exception
)
continue
return validated
if not (validator := CRITICAL_REMOVED_VALIDATORS.get(section)):
raise ValueError(f"Do not know how to validate {section}")
validated = []
for repo_data in data:
try:
validated.append(validator(repo_data))
except vol.Invalid as exception:
LOGGER.info("Got invalid data for %s (%s)", section, exception)
continue
return validated
async def get_repositories(self, section: str) -> list[str]: async def get_repositories(self, section: str) -> list[str]:
"""Get repositories.""" """Get repositories."""

View File

@ -1,4 +1,5 @@
"""Diagnostics support for HACS.""" """Diagnostics support for HACS."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -10,7 +11,6 @@ from homeassistant.core import HomeAssistant
from .base import HacsBase from .base import HacsBase
from .const import DOMAIN from .const import DOMAIN
from .utils.configuration_schema import TOKEN
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
@ -48,8 +48,6 @@ async def async_get_config_entry_diagnostics(
"country", "country",
"debug", "debug",
"dev", "dev",
"experimental",
"netdaemon",
"python_script", "python_script",
"release_limit", "release_limit",
"theme", "theme",
@ -79,4 +77,4 @@ async def async_get_config_entry_diagnostics(
except GitHubException as exception: except GitHubException as exception:
data["rate_limit"] = str(exception) data["rate_limit"] = str(exception)
return async_redact_data(data, (TOKEN,)) return async_redact_data(data, ("token",))

View File

@ -1,4 +1,5 @@
"""HACS Base entities.""" """HACS Base entities."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -7,8 +8,10 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .coordinator import HacsUpdateCoordinator
from .enums import HacsDispatchEvent, HacsGitHubRepo from .enums import HacsDispatchEvent, HacsGitHubRepo
if TYPE_CHECKING: if TYPE_CHECKING:
@ -39,6 +42,10 @@ class HacsBaseEntity(Entity):
"""Initialize.""" """Initialize."""
self.hacs = hacs self.hacs = hacs
class HacsDispatcherEntity(HacsBaseEntity):
"""Base HACS entity listening to dispatcher signals."""
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register for status events.""" """Register for status events."""
self.async_on_remove( self.async_on_remove(
@ -64,7 +71,7 @@ class HacsBaseEntity(Entity):
self.async_write_ha_state() self.async_write_ha_state()
class HacsSystemEntity(HacsBaseEntity): class HacsSystemEntity(HacsDispatcherEntity):
"""Base system entity.""" """Base system entity."""
_attr_icon = "hacs:hacs" _attr_icon = "hacs:hacs"
@ -76,7 +83,7 @@ class HacsSystemEntity(HacsBaseEntity):
return system_info(self.hacs) return system_info(self.hacs)
class HacsRepositoryEntity(HacsBaseEntity): class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
"""Base repository entity.""" """Base repository entity."""
def __init__( def __init__(
@ -85,9 +92,11 @@ class HacsRepositoryEntity(HacsBaseEntity):
repository: HacsRepository, repository: HacsRepository,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(hacs=hacs) BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
HacsBaseEntity.__init__(self, hacs=hacs)
self.repository = repository self.repository = repository
self._attr_unique_id = str(repository.data.id) self._attr_unique_id = str(repository.data.id)
self._repo_last_fetched = repository.data.last_fetched
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -100,20 +109,35 @@ class HacsRepositoryEntity(HacsBaseEntity):
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION: if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
return system_info(self.hacs) return system_info(self.hacs)
def _manufacturer():
if authors := self.repository.data.authors:
return ", ".join(author.replace("@", "") for author in authors)
return self.repository.data.full_name.split("/")[0]
return { return {
"identifiers": {(DOMAIN, str(self.repository.data.id))}, "identifiers": {(DOMAIN, str(self.repository.data.id))},
"name": self.repository.display_name, "name": self.repository.display_name,
"model": self.repository.data.category, "model": self.repository.data.category,
"manufacturer": ", ".join( "manufacturer": _manufacturer(),
author.replace("@", "") for author in self.repository.data.authors "configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}",
),
"configuration_url": "homeassistant://hacs",
"entry_type": DeviceEntryType.SERVICE, "entry_type": DeviceEntryType.SERVICE,
} }
@callback @callback
def _update_and_write_state(self, data: dict) -> None: def _handle_coordinator_update(self) -> None:
"""Update the entity and write state.""" """Handle updated data from the coordinator."""
if data.get("repository_id") == self.repository.data.id: if (
self._update() self._repo_last_fetched is not None
and self.repository.data.last_fetched is not None
and self._repo_last_fetched >= self.repository.data.last_fetched
):
return
self._repo_last_fetched = self.repository.data.last_fetched
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""

View File

@ -1,20 +1,7 @@
"""Helper constants.""" """Helper constants."""
# pylint: disable=missing-class-docstring # pylint: disable=missing-class-docstring
import sys from enum import StrEnum
if sys.version_info.minor >= 11:
# Needs Python 3.11
from enum import StrEnum # # pylint: disable=no-name-in-module
else:
try:
# https://github.com/home-assistant/core/blob/dev/homeassistant/backports/enum.py
# Considered internal to Home Assistant, can be removed whenever.
from homeassistant.backports.enum import StrEnum
except ImportError:
from enum import Enum
class StrEnum(str, Enum):
pass
class HacsGitHubRepo(StrEnum): class HacsGitHubRepo(StrEnum):
@ -29,7 +16,6 @@ class HacsCategory(StrEnum):
INTEGRATION = "integration" INTEGRATION = "integration"
LOVELACE = "lovelace" LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes PLUGIN = "plugin" # Kept for legacy purposes
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script" PYTHON_SCRIPT = "python_script"
TEMPLATE = "template" TEMPLATE = "template"
THEME = "theme" THEME = "theme"
@ -59,11 +45,6 @@ class RepositoryFile(StrEnum):
MAINIFEST_JSON = "manifest.json" MAINIFEST_JSON = "manifest.json"
class ConfigurationType(StrEnum):
YAML = "yaml"
CONFIG_ENTRY = "config_entry"
class LovelaceMode(StrEnum): class LovelaceMode(StrEnum):
"""Lovelace Modes.""" """Lovelace Modes."""

View File

@ -1,61 +1,53 @@
""""Starting setup task: Frontend".""" """Starting setup task: Frontend."""
from __future__ import annotations from __future__ import annotations
import os import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant, callback from homeassistant.components.frontend import (
add_extra_js_url,
async_register_built_in_panel,
)
from .const import DOMAIN, URL_BASE from .const import DOMAIN, URL_BASE
from .hacs_frontend import VERSION as FE_VERSION, locate_dir from .hacs_frontend import VERSION as FE_VERSION, locate_dir
from .hacs_frontend_experimental import ( from .utils.workarounds import async_register_static_path
VERSION as EXPERIMENTAL_FE_VERSION,
locate_dir as experimental_locate_dir,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .base import HacsBase from .base import HacsBase
@callback async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
"""Register the frontend.""" """Register the frontend."""
# Setup themes endpoint if needed
hacs.async_setup_frontend_endpoint_themes()
# Register frontend # Register frontend
if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")): if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")):
hacs.log.warning( hacs.log.warning(
"<HacsFrontend> Frontend development mode enabled. Do not run in production!" "<HacsFrontend> Frontend development mode enabled. Do not run in production!"
) )
hass.http.register_static_path( await async_register_static_path(
f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False hass, f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
)
elif hacs.configuration.experimental:
hacs.log.info("<HacsFrontend> Using experimental frontend")
hass.http.register_static_path(
f"{URL_BASE}/frontend", experimental_locate_dir(), cache_headers=False
) )
hacs.frontend_version = "dev"
else: else:
# await async_register_static_path(
hass.http.register_static_path(f"{URL_BASE}/frontend", locate_dir(), cache_headers=False) hass, f"{URL_BASE}/frontend", locate_dir(), cache_headers=False
)
hacs.frontend_version = FE_VERSION
# Custom iconset # Custom iconset
hass.http.register_static_path( await async_register_static_path(
f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js") hass, f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
)
if "frontend_extra_module_url" not in hass.data:
hass.data["frontend_extra_module_url"] = set()
hass.data["frontend_extra_module_url"].add(f"{URL_BASE}/iconset.js")
hacs.frontend_version = (
FE_VERSION if not hacs.configuration.experimental else EXPERIMENTAL_FE_VERSION
) )
add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
# Add to sidepanel if needed # Add to sidepanel if needed
if DOMAIN not in hass.data.get("frontend_panels", {}): if DOMAIN not in hass.data.get("frontend_panels", {}):
hass.components.frontend.async_register_built_in_panel( async_register_built_in_panel(
hass,
component_name="custom", component_name="custom",
sidebar_title=hacs.configuration.sidepanel_title, sidebar_title=hacs.configuration.sidepanel_title,
sidebar_icon=hacs.configuration.sidepanel_icon, sidebar_icon=hacs.configuration.sidepanel_icon,
@ -72,4 +64,4 @@ def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
) )
# Setup plugin endpoint if needed # Setup plugin endpoint if needed
hacs.async_setup_frontend_endpoint_plugin() await hacs.async_setup_frontend_endpoint_plugin()

View File

@ -1,10 +1 @@
!function(){function n(n){var e=document.createElement("script");e.src=n,document.body.appendChild(e)}if(/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent))n("/hacsfiles/frontend/frontend_es5/entrypoint.c180d0b256f9b6d0.js");else try{new Function("import('/hacsfiles/frontend/frontend_latest/entrypoint.bb9d28f38e9fba76.js')")()}catch(e){n("/hacsfiles/frontend/frontend_es5/entrypoint.c180d0b256f9b6d0.js")}}()
try {
new Function("import('/hacsfiles/frontend/main-ad130be7.js')")();
} catch (err) {
var el = document.createElement('script');
el.src = '/hacsfiles/frontend/main-ad130be7.js';
el.type = 'module';
document.body.appendChild(el);
}

View File

@ -1 +1 @@
VERSION="20220906112053" VERSION="20250128065759"

View File

@ -1,6 +1,9 @@
{ {
"domain": "hacs", "domain": "hacs",
"name": "HACS", "name": "HACS",
"after_dependencies": [
"python_script"
],
"codeowners": [ "codeowners": [
"@ludeeus" "@ludeeus"
], ],
@ -13,11 +16,11 @@
"lovelace", "lovelace",
"repairs" "repairs"
], ],
"documentation": "https://hacs.xyz/docs/configuration/start", "documentation": "https://hacs.xyz/docs/use/",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/hacs/integration/issues", "issue_tracker": "https://github.com/hacs/integration/issues",
"requirements": [ "requirements": [
"aiogithubapi>=22.10.1" "aiogithubapi>=22.10.1"
], ],
"version": "1.33.0" "version": "2.0.5"
} }

View File

@ -1,22 +1,21 @@
"""Initialize repositories.""" """Initialize repositories."""
from __future__ import annotations from __future__ import annotations
from ..enums import HacsCategory from ..enums import HacsCategory
from .appdaemon import HacsAppdaemonRepository from .appdaemon import HacsAppdaemonRepository
from .base import HacsRepository from .base import HacsRepository
from .integration import HacsIntegrationRepository from .integration import HacsIntegrationRepository
from .netdaemon import HacsNetdaemonRepository
from .plugin import HacsPluginRepository from .plugin import HacsPluginRepository
from .python_script import HacsPythonScriptRepository from .python_script import HacsPythonScriptRepository
from .template import HacsTemplateRepository from .template import HacsTemplateRepository
from .theme import HacsThemeRepository from .theme import HacsThemeRepository
RERPOSITORY_CLASSES: dict[HacsCategory, HacsRepository] = { REPOSITORY_CLASSES: dict[HacsCategory, HacsRepository] = {
HacsCategory.THEME: HacsThemeRepository, HacsCategory.THEME: HacsThemeRepository,
HacsCategory.INTEGRATION: HacsIntegrationRepository, HacsCategory.INTEGRATION: HacsIntegrationRepository,
HacsCategory.PYTHON_SCRIPT: HacsPythonScriptRepository, HacsCategory.PYTHON_SCRIPT: HacsPythonScriptRepository,
HacsCategory.APPDAEMON: HacsAppdaemonRepository, HacsCategory.APPDAEMON: HacsAppdaemonRepository,
HacsCategory.NETDAEMON: HacsNetdaemonRepository,
HacsCategory.PLUGIN: HacsPluginRepository, HacsCategory.PLUGIN: HacsPluginRepository,
HacsCategory.TEMPLATE: HacsTemplateRepository, HacsCategory.TEMPLATE: HacsTemplateRepository,
} }

View File

@ -1,4 +1,5 @@
"""Class for appdaemon apps in HACS.""" """Class for appdaemon apps in HACS."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -79,7 +80,7 @@ class HacsAppdaemonRepository(HacsRepository):
# Set local path # Set local path
self.content.path.local = self.localpath self.content.path.local = self.localpath
# Signal entities to refresh # Signal frontend to refresh
if self.data.installed: if self.data.installed:
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,

View File

@ -1,8 +1,9 @@
"""Repository.""" """Repository."""
from __future__ import annotations from __future__ import annotations
from asyncio import sleep from asyncio import sleep
from datetime import datetime from datetime import UTC, datetime
import os import os
import pathlib import pathlib
import shutil import shutil
@ -15,29 +16,31 @@ from aiogithubapi import (
AIOGitHubAPINotModifiedException, AIOGitHubAPINotModifiedException,
GitHubReleaseModel, GitHubReleaseModel,
) )
from aiogithubapi.const import BASE_API_URL
from aiogithubapi.objects.repository import AIOGitHubAPIRepository from aiogithubapi.objects.repository import AIOGitHubAPIRepository
import attr import attr
from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers import device_registry as dr, issue_registry as ir
from ..const import DOMAIN from ..const import DOMAIN
from ..enums import ConfigurationType, HacsDispatchEvent, RepositoryFile from ..enums import HacsDispatchEvent, RepositoryFile
from ..exceptions import ( from ..exceptions import (
HacsException, HacsException,
HacsNotModifiedException, HacsNotModifiedException,
HacsRepositoryArchivedException, HacsRepositoryArchivedException,
HacsRepositoryExistException, HacsRepositoryExistException,
) )
from ..utils.backup import Backup, BackupNetDaemon from ..types import DownloadableContent
from ..utils.backup import Backup
from ..utils.decode import decode_content from ..utils.decode import decode_content
from ..utils.decorator import concurrent from ..utils.decorator import concurrent
from ..utils.file_system import async_exists, async_remove, async_remove_directory
from ..utils.filters import filter_content_return_one_of_type from ..utils.filters import filter_content_return_one_of_type
from ..utils.github_graphql_query import GET_REPOSITORY_RELEASES
from ..utils.json import json_loads from ..utils.json import json_loads
from ..utils.logger import LOGGER from ..utils.logger import LOGGER
from ..utils.path import is_safe from ..utils.path import is_safe
from ..utils.queue_manager import QueueManager from ..utils.queue_manager import QueueManager
from ..utils.store import async_remove_store from ..utils.store import async_remove_store
from ..utils.template import render_template from ..utils.url import github_archive, github_release_asset
from ..utils.validate import Validate from ..utils.validate import Validate
from ..utils.version import ( from ..utils.version import (
version_left_higher_or_equal_then_right, version_left_higher_or_equal_then_right,
@ -83,7 +86,6 @@ TOPIC_FILTER = (
"lovelace", "lovelace",
"media-player", "media-player",
"mediaplayer", "mediaplayer",
"netdaemon",
"plugin", "plugin",
"python_script", "python_script",
"python-script", "python-script",
@ -112,6 +114,7 @@ REPOSITORY_KEYS_TO_EXPORT = (
("last_version", None), ("last_version", None),
("manifest_name", None), ("manifest_name", None),
("open_issues", 0), ("open_issues", 0),
("prerelease", None),
("stargazers_count", 0), ("stargazers_count", 0),
("topics", []), ("topics", []),
) )
@ -163,6 +166,7 @@ class RepositoryData:
manifest_name: str = None manifest_name: str = None
new: bool = True new: bool = True
open_issues: int = 0 open_issues: int = 0
prerelease: str = None
published_tags: list[str] = [] published_tags: list[str] = []
releases: bool = False releases: bool = False
selected_tag: str = None selected_tag: str = None
@ -173,7 +177,7 @@ class RepositoryData:
@property @property
def name(self): def name(self):
"""Return the name.""" """Return the name."""
if self.category in ["integration", "netdaemon"]: if self.category == "integration":
return self.domain return self.domain
return self.full_name.split("/")[-1] return self.full_name.split("/")[-1]
@ -195,7 +199,7 @@ class RepositoryData:
continue continue
if key == "last_fetched" and isinstance(value, float): if key == "last_fetched" and isinstance(value, float):
setattr(self, key, datetime.fromtimestamp(value)) setattr(self, key, datetime.fromtimestamp(value, UTC))
elif key == "id": elif key == "id":
setattr(self, key, str(value)) setattr(self, key, str(value))
elif key == "country": elif key == "country":
@ -383,7 +387,9 @@ class HacsRepository:
@property @property
def display_available_version(self) -> str: def display_available_version(self) -> str:
"""Return display_authors""" """Return display_authors"""
if self.data.last_version is not None: if self.data.show_beta and self.data.prerelease is not None:
available = self.data.prerelease
elif self.data.last_version is not None:
available = self.data.last_version available = self.data.last_version
else: else:
if self.data.last_commit is not None: if self.data.last_commit is not None:
@ -404,8 +410,6 @@ class HacsRepository:
@property @property
def pending_update(self) -> bool: def pending_update(self) -> bool:
"""Return True if pending update.""" """Return True if pending update."""
if not self.can_download:
return False
if self.data.installed: if self.data.installed:
if self.data.selected_tag is not None: if self.data.selected_tag is not None:
if self.data.selected_tag == self.data.default_branch: if self.data.selected_tag == self.data.default_branch:
@ -500,13 +504,7 @@ class HacsRepository:
if self.repository_object: if self.repository_object:
self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0) self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0)
self.data.last_fetched = datetime.utcnow() self.data.last_fetched = datetime.now(UTC)
# Set topics
self.data.topics = self.data.topics
# Set description
self.data.description = self.data.description
@concurrent(concurrenttasks=10, backoff_time=5) @concurrent(concurrenttasks=10, backoff_time=5)
async def common_update(self, ignore_issues=False, force=False, skip_releases=False) -> bool: async def common_update(self, ignore_issues=False, force=False, skip_releases=False) -> bool:
@ -554,53 +552,56 @@ class HacsRepository:
self.additional_info = await self.async_get_info_file_contents() self.additional_info = await self.async_get_info_file_contents()
# Set last fetch attribute # Set last fetch attribute
self.data.last_fetched = datetime.utcnow() self.data.last_fetched = datetime.now(UTC)
return True return True
async def download_zip_files(self, validate) -> None: async def download_zip_files(self, validate: Validate) -> None:
"""Download ZIP archive from repository release.""" """Download ZIP archive from repository release."""
try:
contents = None
target_ref = self.ref.split("/")[1]
for release in self.releases.objects: try:
self.logger.debug( await self.async_download_zip_file(
"%s ref: %s --- tag: %s", self.string, target_ref, release.tag_name DownloadableContent(
name=self.repository_manifest.filename,
url=github_release_asset(
repository=self.data.full_name,
version=self.ref,
filename=self.repository_manifest.filename,
),
),
validate,
)
# lgtm [py/catch-base-exception] pylint: disable=broad-except
except BaseException:
validate.errors.append(
f"Download of {
self.repository_manifest.filename} was not completed"
) )
if release.tag_name == target_ref:
contents = release.assets
break
if not contents: async def async_download_zip_file(
validate.errors.append(f"No assets found for release '{self.ref}'") self,
return content: DownloadableContent,
validate: Validate,
download_queue = QueueManager(hass=self.hacs.hass) ) -> None:
for content in contents or []:
download_queue.add(self.async_download_zip_file(content, validate))
await download_queue.execute()
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
validate.errors.append("Download was not completed")
async def async_download_zip_file(self, content, validate) -> None:
"""Download ZIP archive from repository release.""" """Download ZIP archive from repository release."""
try: try:
filecontent = await self.hacs.async_download_file(content.browser_download_url) filecontent = await self.hacs.async_download_file(content["url"])
if filecontent is None: if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded") validate.errors.append(f"Failed to download {content['url']}")
return return
temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp) temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp)
temp_file = f"{temp_dir}/{self.repository_manifest.filename}" temp_file = f"{temp_dir}/{self.repository_manifest.filename}"
result = await self.hacs.async_save_file(temp_file, filecontent) result = await self.hacs.async_save_file(temp_file, filecontent)
def _extract_zip_file():
with zipfile.ZipFile(temp_file, "r") as zip_file: with zipfile.ZipFile(temp_file, "r") as zip_file:
zip_file.extractall(self.content.path.local) zip_file.extractall(self.content.path.local)
await self.hacs.hass.async_add_executor_job(_extract_zip_file)
def cleanup_temp_dir(): def cleanup_temp_dir():
"""Cleanup temp_dir.""" """Cleanup temp_dir."""
if os.path.exists(temp_dir): if os.path.exists(temp_dir):
@ -608,32 +609,39 @@ class HacsRepository:
shutil.rmtree(temp_dir) shutil.rmtree(temp_dir)
if result: if result:
self.logger.info("%s Download of %s completed", self.string, content.name) self.logger.info("%s Download of %s completed", self.string, content["name"])
await self.hacs.hass.async_add_executor_job(cleanup_temp_dir) await self.hacs.hass.async_add_executor_job(cleanup_temp_dir)
return return
validate.errors.append(f"[{content.name}] was not downloaded") validate.errors.append(f"[{content['name']}] was not downloaded")
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
except BaseException:
validate.errors.append("Download was not completed") validate.errors.append("Download was not completed")
async def download_content(self) -> None: async def download_content(self, version: string | None = None) -> None:
"""Download the content of a directory.""" """Download the content of a directory."""
if self.hacs.configuration.experimental: contents: list[FileInformation] | None = None
if ( if (
not self.repository_manifest.zip_release not self.repository_manifest.zip_release
and not self.data.file_name and not self.data.file_name
and self.content.path.remote is not None and self.content.path.remote is not None
): ):
self.logger.info("%s Trying experimental download", self.string) self.logger.info("%s Downloading repository archive", self.string)
try: try:
await self.download_repository_zip() await self.download_repository_zip()
return return
except HacsException as exception: except HacsException as exception:
self.logger.exception(exception) self.logger.exception(exception)
contents = self.gather_files_to_download()
if self.repository_manifest.filename: if self.repository_manifest.filename:
self.logger.debug("%s %s", self.string, self.repository_manifest.filename) self.logger.debug("%s %s", self.string, self.repository_manifest.filename)
if self.content.path.remote == "release" and version is not None:
contents = await self.release_contents(version)
if not contents:
contents = self.gather_files_to_download()
if not contents: if not contents:
raise HacsException("No content to download") raise HacsException("No content to download")
@ -654,14 +662,16 @@ class HacsRepository:
if not ref: if not ref:
raise HacsException("Missing required elements.") raise HacsException("Missing required elements.")
url = f"{BASE_API_URL}/repos/{self.data.full_name}/zipball/{ref}"
filecontent = await self.hacs.async_download_file( filecontent = await self.hacs.async_download_file(
url, github_archive(repository=self.data.full_name, version=ref, variant="tags"),
headers={ keep_url=True,
"Authorization": f"token {self.hacs.configuration.token}", nolog=True,
"User-Agent": f"HACS/{self.hacs.version}", )
},
if filecontent is None:
filecontent = await self.hacs.async_download_file(
github_archive(repository=self.data.full_name, version=ref, variant="heads"),
keep_url=True,
) )
if filecontent is None: if filecontent is None:
raise HacsException(f"[{self}] Failed to download zipball") raise HacsException(f"[{self}] Failed to download zipball")
@ -672,6 +682,7 @@ class HacsRepository:
if not result: if not result:
raise HacsException("Could not save ZIP file") raise HacsException("Could not save ZIP file")
def _extract_zip_file():
with zipfile.ZipFile(temp_file, "r") as zip_file: with zipfile.ZipFile(temp_file, "r") as zip_file:
extractable = [] extractable = []
for path in zip_file.filelist: for path in zip_file.filelist:
@ -681,10 +692,17 @@ class HacsRepository:
and filename != self.content.path.remote and filename != self.content.path.remote
): ):
path.filename = filename.replace(self.content.path.remote, "") path.filename = filename.replace(self.content.path.remote, "")
if path.filename == "/":
# Blank files is not valid, and will start to throw in Python 3.12
continue
extractable.append(path) extractable.append(path)
if len(extractable) == 0:
raise HacsException("No content to extract")
zip_file.extractall(self.content.path.local, extractable) zip_file.extractall(self.content.path.local, extractable)
await self.hacs.hass.async_add_executor_job(_extract_zip_file)
def cleanup_temp_dir(): def cleanup_temp_dir():
"""Cleanup temp_dir.""" """Cleanup temp_dir."""
if os.path.exists(temp_dir): if os.path.exists(temp_dir):
@ -706,18 +724,15 @@ class HacsRepository:
) )
if response: if response:
return json_loads(decode_content(response.data.content)) return json_loads(decode_content(response.data.content))
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
except BaseException:
pass pass
async def async_get_info_file_contents(self) -> str: async def async_get_info_file_contents(self, *, version: str | None = None, **kwargs) -> str:
"""Get the content of the info.md file.""" """Get the content of the info.md file."""
def _info_file_variants() -> tuple[str, ...]: def _info_file_variants() -> tuple[str, ...]:
name: str = ( name: str = "readme"
"readme"
if self.repository_manifest.render_readme or self.hacs.configuration.experimental
else "info"
)
return ( return (
f"{name.upper()}.md", f"{name.upper()}.md",
f"{name}.md", f"{name}.md",
@ -732,25 +747,7 @@ class HacsRepository:
if not info_files: if not info_files:
return "" return ""
try: return await self.get_documentation(filename=info_files[0], version=version) or ""
response = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.repos.contents.get,
raise_exception=False,
repository=self.data.full_name,
path=info_files[0],
)
if response:
return render_template(
self.hacs,
decode_content(response.data.content)
.replace("<svg", "<disabled")
.replace("</svg", "</disabled"),
self,
)
except BaseException as exc: # lgtm [py/catch-base-exception] pylint: disable=broad-except
self.logger.error("%s %s", self.string, exc)
return ""
def remove(self) -> None: def remove(self) -> None:
"""Run remove tasks.""" """Run remove tasks."""
@ -764,19 +761,7 @@ class HacsRepository:
if not await self.remove_local_directory(): if not await self.remove_local_directory():
raise HacsException("Could not uninstall") raise HacsException("Could not uninstall")
self.data.installed = False self.data.installed = False
if self.data.category == "integration": await self._async_post_uninstall()
if self.data.config_flow:
await self.reload_custom_components()
else:
self.pending_restart = True
elif self.data.category == "theme":
try:
await self.hacs.hass.services.async_call("frontend", "reload_themes", {})
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
pass
elif self.data.category == "template":
await self.hacs.hass.services.async_call("homeassistant", "reload_custom_templates", {})
await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs") await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs")
self.data.installed_version = None self.data.installed_version = None
@ -799,7 +784,7 @@ class HacsRepository:
try: try:
if self.data.category == "python_script": if self.data.category == "python_script":
local_path = f"{self.content.path.local}/{self.data.name}.py" local_path = f"{self.content.path.local}/{self.data.file_name}"
elif self.data.category == "template": elif self.data.category == "template":
local_path = f"{self.content.path.local}/{self.data.file_name}" local_path = f"{self.content.path.local}/{self.data.file_name}"
elif self.data.category == "theme": elif self.data.category == "theme":
@ -808,8 +793,7 @@ class HacsRepository:
f"{self.hacs.configuration.theme_path}/" f"{self.hacs.configuration.theme_path}/"
f"{self.data.name}.yaml" f"{self.data.name}.yaml"
) )
if os.path.exists(path): await async_remove(self.hacs.hass, path, missing_ok=True)
os.remove(path)
local_path = self.content.path.local local_path = self.content.path.local
elif self.data.category == "integration": elif self.data.category == "integration":
if not self.data.domain: if not self.data.domain:
@ -823,18 +807,18 @@ class HacsRepository:
else: else:
local_path = self.content.path.local local_path = self.content.path.local
if os.path.exists(local_path): if await async_exists(self.hacs.hass, local_path):
if not is_safe(self.hacs, local_path): if not is_safe(self.hacs, local_path):
self.logger.error("%s Path %s is blocked from removal", self.string, local_path) self.logger.error("%s Path %s is blocked from removal", self.string, local_path)
return False return False
self.logger.debug("%s Removing %s", self.string, local_path) self.logger.debug("%s Removing %s", self.string, local_path)
if self.data.category in ["python_script", "template"]: if self.data.category in ["python_script", "template"]:
os.remove(local_path) await async_remove(self.hacs.hass, local_path)
else: else:
shutil.rmtree(local_path) await async_remove_directory(self.hacs.hass, local_path)
while os.path.exists(local_path): while await async_exists(self.hacs.hass, local_path):
await sleep(1) await sleep(1)
else: else:
self.logger.debug( self.logger.debug(
@ -842,7 +826,8 @@ class HacsRepository:
) )
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.logger.debug("%s Removing %s failed with %s", self.string, local_path, exception) self.logger.debug("%s Removing %s failed with %s", self.string, local_path, exception)
return False return False
@ -888,7 +873,7 @@ class HacsRepository:
await self.async_pre_install() await self.async_pre_install()
self.logger.info("%s Pre installation steps completed", self.string) self.logger.info("%s Pre installation steps completed", self.string)
async def async_install(self) -> None: async def async_install(self, *, version: str | None = None, **_) -> None:
"""Run install steps.""" """Run install steps."""
await self._async_pre_install() await self._async_pre_install()
self.hacs.async_dispatch( self.hacs.async_dispatch(
@ -896,7 +881,7 @@ class HacsRepository:
{"repository": self.data.full_name, "progress": 30}, {"repository": self.data.full_name, "progress": 30},
) )
self.logger.info("%s Running installation steps", self.string) self.logger.info("%s Running installation steps", self.string)
await self.async_install_repository() await self.async_install_repository(version=version)
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 90}, {"repository": self.data.full_name, "progress": 90},
@ -911,6 +896,13 @@ class HacsRepository:
async def async_post_installation(self) -> None: async def async_post_installation(self) -> None:
"""Run post install steps.""" """Run post install steps."""
async def async_post_uninstall(self):
"""Run post uninstall steps."""
async def _async_post_uninstall(self):
"""Run post uninstall steps."""
await self.async_post_uninstall()
async def _async_post_install(self) -> None: async def _async_post_install(self) -> None:
"""Run post install steps.""" """Run post install steps."""
self.logger.info("%s Running post installation steps", self.string) self.logger.info("%s Running post installation steps", self.string)
@ -927,39 +919,34 @@ class HacsRepository:
) )
self.logger.info("%s Post installation steps completed", self.string) self.logger.info("%s Post installation steps completed", self.string)
async def async_install_repository(self) -> None: async def async_install_repository(self, *, version: str | None = None, **_) -> None:
"""Common installation steps of the repository.""" """Common installation steps of the repository."""
persistent_directory = None persistent_directory = None
await self.update_repository(force=True) await self.update_repository(force=version is None)
if self.content.path.local is None: if self.content.path.local is None:
raise HacsException("repository.content.path.local is None") raise HacsException("repository.content.path.local is None")
self.validate.errors.clear() self.validate.errors.clear()
if not self.can_download: version_to_install = version or self.version_to_download()
raise HacsException("The version of Home Assistant is not compatible with this version") if version_to_install == self.data.default_branch:
self.ref = version_to_install
version = self.version_to_download()
if version == self.data.default_branch:
self.ref = version
else: else:
self.ref = f"tags/{version}" self.ref = f"tags/{version_to_install}"
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 40}, {"repository": self.data.full_name, "progress": 40},
) )
if self.data.installed and self.data.category == "netdaemon": if self.repository_manifest.persistent_directory:
persistent_directory = BackupNetDaemon(hacs=self.hacs, repository=self) if await async_exists(
await self.hacs.hass.async_add_executor_job(persistent_directory.create) self.hacs.hass,
f"{self.content.path.local}/{self.repository_manifest.persistent_directory}",
elif self.repository_manifest.persistent_directory:
if os.path.exists(
f"{self.content.path.local}/{self.repository_manifest.persistent_directory}"
): ):
persistent_directory = Backup( persistent_directory = Backup(
hacs=self.hacs, hacs=self.hacs,
local_path=f"{self.content.path.local}/{self.repository_manifest.persistent_directory}", local_path=f"{
self.content.path.local}/{self.repository_manifest.persistent_directory}",
backup_path=tempfile.gettempdir() + "/hacs_persistent_directory/", backup_path=tempfile.gettempdir() + "/hacs_persistent_directory/",
) )
await self.hacs.hass.async_add_executor_job(persistent_directory.create) await self.hacs.hass.async_add_executor_job(persistent_directory.create)
@ -970,16 +957,17 @@ class HacsRepository:
self.hacs.log.debug("%s Local path is set to %s", self.string, self.content.path.local) self.hacs.log.debug("%s Local path is set to %s", self.string, self.content.path.local)
self.hacs.log.debug("%s Remote path is set to %s", self.string, self.content.path.remote) self.hacs.log.debug("%s Remote path is set to %s", self.string, self.content.path.remote)
self.hacs.log.debug("%s Version to install: %s", self.string, version_to_install)
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 50}, {"repository": self.data.full_name, "progress": 50},
) )
if self.repository_manifest.zip_release and version != self.data.default_branch: if self.repository_manifest.zip_release and self.repository_manifest.filename:
await self.download_zip_files(self.validate) await self.download_zip_files(self.validate)
else: else:
await self.download_content() await self.download_content(version_to_install)
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
@ -1010,10 +998,10 @@ class HacsRepository:
self.data.installed = True self.data.installed = True
self.data.installed_commit = self.data.last_commit self.data.installed_commit = self.data.last_commit
if version == self.data.default_branch: if version_to_install == self.data.default_branch:
self.data.installed_version = None self.data.installed_version = None
else: else:
self.data.installed_version = version self.data.installed_version = version_to_install
async def async_get_legacy_repository_object( async def async_get_legacy_repository_object(
self, self,
@ -1071,9 +1059,9 @@ class HacsRepository:
) )
self.repository_object = repository_object self.repository_object = repository_object
if self.data.full_name.lower() != repository_object.full_name.lower(): if self.data.full_name.lower() != repository_object.full_name.lower():
self.hacs.common.renamed_repositories[ self.hacs.common.renamed_repositories[self.data.full_name] = (
self.data.full_name repository_object.full_name
] = repository_object.full_name )
if not self.hacs.system.generator: if not self.hacs.system.generator:
raise HacsRepositoryExistException raise HacsRepositoryExistException
self.logger.error( self.logger.error(
@ -1089,7 +1077,7 @@ class HacsRepository:
except HacsRepositoryExistException: except HacsRepositoryExistException:
raise HacsRepositoryExistException from None raise HacsRepositoryExistException from None
except (AIOGitHubAPIException, HacsException) as exception: except (AIOGitHubAPIException, HacsException) as exception:
if not self.hacs.status.startup: if not self.hacs.status.startup or self.hacs.system.generator:
self.logger.error("%s %s", self.string, exception) self.logger.error("%s %s", self.string, exception)
if not ignore_issues: if not ignore_issues:
self.validate.errors.append("Repository does not exist.") self.validate.errors.append("Repository does not exist.")
@ -1112,15 +1100,28 @@ class HacsRepository:
# Get releases. # Get releases.
if not skip_releases: if not skip_releases:
try: try:
releases = await self.get_releases( releases = await self.get_releases(prerelease=True, returnlimit=30)
prerelease=self.data.show_beta,
returnlimit=self.hacs.configuration.release_limit,
)
if releases: if releases:
self.data.prerelease = None
for release in releases:
if release.draft:
continue
elif release.prerelease:
if self.data.prerelease is None:
self.data.prerelease = release.tag_name
else:
self.data.last_version = release.tag_name
break
self.data.releases = True self.data.releases = True
self.releases.objects = releases
self.data.published_tags = [x.tag_name for x in self.releases.objects] filtered_releases = [
self.data.last_version = next(iter(self.data.published_tags)) release
for release in releases
if not release.draft and (self.data.show_beta or not release.prerelease)
]
self.releases.objects = filtered_releases
self.data.published_tags = [x.tag_name for x in filtered_releases]
except HacsException: except HacsException:
self.data.releases = False self.data.releases = False
@ -1228,6 +1229,25 @@ class HacsRepository:
files.append(FileInformation(path.download_url, path.full_path, path.filename)) files.append(FileInformation(path.download_url, path.full_path, path.filename))
return files return files
async def release_contents(self, version: str | None = None) -> list[FileInformation] | None:
"""Gather the contents of a release."""
release = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.generic,
endpoint=f"/repos/{self.data.full_name}/releases/tags/{version}",
raise_exception=False,
)
if release is None:
return None
return [
FileInformation(
url=asset.get("browser_download_url"),
path=asset.get("name"),
name=asset.get("name"),
)
for asset in release.data.get("assets", [])
]
@concurrent(concurrenttasks=10) @concurrent(concurrenttasks=10)
async def dowload_repository_content(self, content: FileInformation) -> None: async def dowload_repository_content(self, content: FileInformation) -> None:
"""Download content.""" """Download content."""
@ -1266,18 +1286,13 @@ class HacsRepository:
self.validate.errors.append(f"[{content.name}] was not downloaded.") self.validate.errors.append(f"[{content.name}] was not downloaded.")
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.validate.errors.append(f"Download was not completed [{exception}]") self.validate.errors.append(f"Download was not completed [{exception}]")
async def async_remove_entity_device(self) -> None: async def async_remove_entity_device(self) -> None:
"""Remove the entity device.""" """Remove the entity device."""
if (
self.hacs.configuration == ConfigurationType.YAML
or not self.hacs.configuration.experimental
):
return
device_registry: dr.DeviceRegistry = dr.async_get(hass=self.hacs.hass) device_registry: dr.DeviceRegistry = dr.async_get(hass=self.hacs.hass)
device = device_registry.async_get_device(identifiers={(DOMAIN, str(self.data.id))}) device = device_registry.async_get_device(identifiers={(DOMAIN, str(self.data.id))})
@ -1303,3 +1318,137 @@ class HacsRepository:
return self.data.selected_tag return self.data.selected_tag
return self.data.default_branch or "main" return self.data.default_branch or "main"
async def get_documentation(
self,
*,
filename: str | None = None,
version: str | None = None,
**kwargs,
) -> str | None:
"""Get the documentation of the repository."""
if filename is None:
return None
if version is not None:
target_version = version
elif self.data.installed:
target_version = self.data.installed_version or self.data.installed_commit
else:
target_version = self.data.last_version or self.data.last_commit or self.ref
self.logger.debug(
"%s Getting documentation for version=%s,filename=%s",
self.string,
target_version,
filename,
)
if target_version is None:
return None
result = await self.hacs.async_download_file(
f"https://raw.githubusercontent.com/{
self.data.full_name}/{target_version}/{filename}",
nolog=True,
)
return (
result.decode(encoding="utf-8")
.replace("<svg", "<disabled")
.replace("</svg", "</disabled")
if result
else None
)
async def get_hacs_json(self, *, version: str, **kwargs) -> HacsManifest | None:
"""Get the hacs.json file of the repository."""
self.logger.debug("%s Getting hacs.json for version=%s", self.string, version)
try:
result = await self.hacs.async_download_file(
f"https://raw.githubusercontent.com/{
self.data.full_name}/{version}/hacs.json",
nolog=True,
)
if result is None:
return None
return HacsManifest.from_dict(json_loads(result))
except Exception: # pylint: disable=broad-except
return None
async def _ensure_download_capabilities(self, ref: str | None, **kwargs: Any) -> None:
"""Ensure that the download can be handled."""
target_manifest: HacsManifest | None = None
if ref is None:
if not self.can_download:
raise HacsException(
f"This {
self.data.category.value} is not available for download."
)
return
if ref == self.data.last_version:
target_manifest = self.repository_manifest
else:
target_manifest = await self.get_hacs_json(version=ref)
if target_manifest is None:
raise HacsException(
f"The version {ref} for this {
self.data.category.value} can not be used with HACS."
)
if (
target_manifest.homeassistant is not None
and self.hacs.core.ha_version < target_manifest.homeassistant
):
raise HacsException(
f"This version requires Home Assistant {
target_manifest.homeassistant} or newer."
)
if target_manifest.hacs is not None and self.hacs.version < target_manifest.hacs:
raise HacsException(f"This version requires HACS {
target_manifest.hacs} or newer.")
async def async_download_repository(self, *, ref: str | None = None, **_) -> None:
"""Download the content of a repository."""
await self._ensure_download_capabilities(ref)
self.logger.info("Starting download, %s", ref)
if self.display_version_or_commit == "version":
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 10},
)
if not ref:
await self.update_repository(force=True)
else:
self.ref = ref
self.data.selected_tag = ref
self.force_branch = ref is not None
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 20},
)
try:
await self.async_install(version=ref)
except HacsException as exception:
raise HacsException(
f"Downloading {self.data.full_name} with version {
ref or self.data.last_version or self.data.last_commit} failed with ({exception})"
) from exception
finally:
self.data.selected_tag = None
self.force_branch = False
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": False},
)
async def async_get_releases(self, *, first: int = 30) -> list[GitHubReleaseModel]:
"""Get the last x releases of a repository."""
response = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.repos.releases.list,
repository=self.data.full_name,
kwargs={"per_page": 30},
)
return response.data

View File

@ -1,4 +1,5 @@
"""Class for integrations in HACS.""" """Class for integrations in HACS."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -45,7 +46,7 @@ class HacsIntegrationRepository(HacsRepository):
if self.data.first_install: if self.data.first_install:
self.pending_restart = False self.pending_restart = False
if self.pending_restart and self.hacs.configuration.experimental: if self.pending_restart:
self.logger.debug("%s Creating restart_required issue", self.string) self.logger.debug("%s Creating restart_required issue", self.string)
async_create_issue( async_create_issue(
hass=self.hacs.hass, hass=self.hacs.hass,
@ -60,6 +61,13 @@ class HacsIntegrationRepository(HacsRepository):
}, },
) )
async def async_post_uninstall(self) -> None:
"""Run post uninstall steps."""
if self.data.config_flow:
await self.reload_custom_components()
else:
self.pending_restart = True
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
await self.common_validate() await self.common_validate()
@ -78,7 +86,8 @@ class HacsIntegrationRepository(HacsRepository):
): ):
raise AddonRepositoryException() raise AddonRepositoryException()
raise HacsException( raise HacsException(
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" f"{self.string} Repository structure for {
self.ref.replace('tags/', '')} is not compliant"
) )
self.content.path.remote = f"custom_components/{name}" self.content.path.remote = f"custom_components/{name}"
@ -93,7 +102,8 @@ class HacsIntegrationRepository(HacsRepository):
except KeyError as exception: except KeyError as exception:
self.validate.errors.append( self.validate.errors.append(
f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}" f"Missing expected key '{exception}' in {
RepositoryFile.MAINIFEST_JSON}"
) )
self.hacs.log.error( self.hacs.log.error(
"Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
@ -133,7 +143,8 @@ class HacsIntegrationRepository(HacsRepository):
except KeyError as exception: except KeyError as exception:
self.validate.errors.append( self.validate.errors.append(
f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}" f"Missing expected key '{exception}' in {
RepositoryFile.MAINIFEST_JSON}"
) )
self.hacs.log.error( self.hacs.log.error(
"Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
@ -142,7 +153,7 @@ class HacsIntegrationRepository(HacsRepository):
# Set local path # Set local path
self.content.path.local = self.localpath self.content.path.local = self.localpath
# Signal entities to refresh # Signal frontend to refresh
if self.data.installed: if self.data.installed:
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,
@ -180,3 +191,27 @@ class HacsIntegrationRepository(HacsRepository):
) )
if response: if response:
return json_loads(decode_content(response.data.content)) return json_loads(decode_content(response.data.content))
async def get_integration_manifest(self, *, version: str, **kwargs) -> dict[str, Any] | None:
"""Get the content of the manifest.json file."""
manifest_path = (
"manifest.json"
if self.repository_manifest.content_in_root
else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}"
)
if manifest_path not in (x.full_path for x in self.tree):
raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'")
self.logger.debug("%s Getting manifest.json for version=%s", self.string, version)
try:
result = await self.hacs.async_download_file(
f"https://raw.githubusercontent.com/{
self.data.full_name}/{version}/{manifest_path}",
nolog=True,
)
if result is None:
return None
return json_loads(result)
except Exception: # pylint: disable=broad-except
return None

View File

@ -1,6 +1,8 @@
"""Class for plugins in HACS.""" """Class for plugins in HACS."""
from __future__ import annotations from __future__ import annotations
import re
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ..enums import HacsCategory, HacsDispatchEvent from ..enums import HacsCategory, HacsDispatchEvent
@ -9,7 +11,11 @@ from ..utils.decorator import concurrent
from ..utils.json import json_loads from ..utils.json import json_loads
from .base import HacsRepository from .base import HacsRepository
HACSTAG_REPLACER = re.compile(r"\D+")
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.components.lovelace.resources import ResourceStorageCollection
from ..base import HacsBase from ..base import HacsBase
@ -55,7 +61,12 @@ class HacsPluginRepository(HacsRepository):
async def async_post_installation(self): async def async_post_installation(self):
"""Run post installation steps.""" """Run post installation steps."""
self.hacs.async_setup_frontend_endpoint_plugin() await self.hacs.async_setup_frontend_endpoint_plugin()
await self.update_dashboard_resources()
async def async_post_uninstall(self):
"""Run post uninstall steps."""
await self.remove_dashboard_resources()
@concurrent(concurrenttasks=10, backoff_time=5) @concurrent(concurrenttasks=10, backoff_time=5)
async def update_repository(self, ignore_issues=False, force=False): async def update_repository(self, ignore_issues=False, force=False):
@ -74,7 +85,7 @@ class HacsPluginRepository(HacsRepository):
if self.content.path.remote == "release": if self.content.path.remote == "release":
self.content.single = True self.content.single = True
# Signal entities to refresh # Signal frontend to refresh
if self.data.installed: if self.data.installed:
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,
@ -99,9 +110,9 @@ class HacsPluginRepository(HacsRepository):
def update_filenames(self) -> None: def update_filenames(self) -> None:
"""Get the filename to target.""" """Get the filename to target."""
# Handler for plug requirement 3 content_in_root = self.repository_manifest.content_in_root
if self.repository_manifest.filename: if specific_filename := self.repository_manifest.filename:
valid_filenames = (self.repository_manifest.filename,) valid_filenames = (specific_filename,)
else: else:
valid_filenames = ( valid_filenames = (
f"{self.data.name.replace('lovelace-', '')}.js", f"{self.data.name.replace('lovelace-', '')}.js",
@ -110,7 +121,7 @@ class HacsPluginRepository(HacsRepository):
f"{self.data.name}-bundle.js", f"{self.data.name}-bundle.js",
) )
if not self.repository_manifest.content_in_root: if not content_in_root:
if self.releases.objects: if self.releases.objects:
release = self.releases.objects[0] release = self.releases.objects[0]
if release.assets: if release.assets:
@ -124,11 +135,112 @@ class HacsPluginRepository(HacsRepository):
self.content.path.remote = "release" self.content.path.remote = "release"
return return
for location in ("",) if self.repository_manifest.content_in_root else ("dist", ""): all_paths = {x.full_path for x in self.tree}
for filename in valid_filenames: for filename in valid_filenames:
if f"{location+'/' if location else ''}{filename}" in [ if filename in all_paths:
x.full_path for x in self.tree self.data.file_name = filename
]: self.content.path.remote = ""
return
if not content_in_root and f"dist/{filename}" in all_paths:
self.data.file_name = filename.split("/")[-1] self.data.file_name = filename.split("/")[-1]
self.content.path.remote = location self.content.path.remote = "dist"
break return
def generate_dashboard_resource_hacstag(self) -> str:
"""Get the HACS tag used by dashboard resources."""
version = (
self.display_installed_version
or self.data.selected_tag
or self.display_available_version
)
return f"{self.data.id}{HACSTAG_REPLACER.sub('', version)}"
def generate_dashboard_resource_namespace(self) -> str:
"""Get the dashboard resource namespace."""
return f"/hacsfiles/{self.data.full_name.split("/")[1]}"
def generate_dashboard_resource_url(self) -> str:
"""Get the dashboard resource namespace."""
filename = self.data.file_name
if "/" in filename:
self.logger.warning("%s have defined an invalid file name %s", self.string, filename)
filename = filename.split("/")[-1]
return (
f"{self.generate_dashboard_resource_namespace()}/{filename}"
f"?hacstag={self.generate_dashboard_resource_hacstag()}"
)
def _get_resource_handler(self) -> ResourceStorageCollection | None:
"""Get the resource handler."""
resources: ResourceStorageCollection | None
if not (hass_data := self.hacs.hass.data):
self.logger.error("%s Can not access the hass data", self.string)
return
if (lovelace_data := hass_data.get("lovelace")) is None:
self.logger.warning("%s Can not access the lovelace integration data", self.string)
return
if self.hacs.core.ha_version > "2025.1.99":
# Changed to 2025.2.0
# Changed in https://github.com/home-assistant/core/pull/136313
resources = lovelace_data.resources
else:
resources = lovelace_data.get("resources")
if resources is None:
self.logger.warning("%s Can not access the dashboard resources", self.string)
return
if not hasattr(resources, "store") or resources.store is None:
self.logger.info("%s YAML mode detected, can not update resources", self.string)
return
if resources.store.key != "lovelace_resources" or resources.store.version != 1:
self.logger.warning("%s Can not use the dashboard resources", self.string)
return
return resources
async def update_dashboard_resources(self) -> None:
"""Update dashboard resources."""
if not (resources := self._get_resource_handler()):
return
if not resources.loaded:
await resources.async_load()
namespace = self.generate_dashboard_resource_namespace()
url = self.generate_dashboard_resource_url()
for entry in resources.async_items():
if (entry_url := entry["url"]).startswith(namespace):
if entry_url != url:
self.logger.info(
"%s Updating existing dashboard resource from %s to %s",
self.string,
entry_url,
url,
)
await resources.async_update_item(entry["id"], {"url": url})
return
# Nothing was updated, add the resource
self.logger.info("%s Adding dashboard resource %s", self.string, url)
await resources.async_create_item({"res_type": "module", "url": url})
async def remove_dashboard_resources(self) -> None:
"""Remove dashboard resources."""
if not (resources := self._get_resource_handler()):
return
if not resources.loaded:
await resources.async_load()
namespace = self.generate_dashboard_resource_namespace()
for entry in resources.async_items():
if entry["url"].startswith(namespace):
self.logger.info("%s Removing dashboard resource %s", self.string, entry["url"])
await resources.async_delete_item(entry["id"])
return

View File

@ -1,4 +1,5 @@
"""Class for python_scripts in HACS.""" """Class for python_scripts in HACS."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -89,7 +90,7 @@ class HacsPythonScriptRepository(HacsRepository):
# Update name # Update name
self.update_filenames() self.update_filenames()
# Signal entities to refresh # Signal frontend to refresh
if self.data.installed: if self.data.installed:
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,

View File

@ -1,8 +1,11 @@
"""Class for themes in HACS.""" """Class for themes in HACS."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from homeassistant.exceptions import HomeAssistantError
from ..enums import HacsCategory, HacsDispatchEvent from ..enums import HacsCategory, HacsDispatchEvent
from ..exceptions import HacsException from ..exceptions import HacsException
from ..utils.decorator import concurrent from ..utils.decorator import concurrent
@ -32,7 +35,7 @@ class HacsTemplateRepository(HacsRepository):
async def async_post_installation(self): async def async_post_installation(self):
"""Run post installation steps.""" """Run post installation steps."""
await self.hacs.hass.services.async_call("homeassistant", "reload_custom_templates", {}) await self._reload_custom_templates()
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
@ -68,6 +71,18 @@ class HacsTemplateRepository(HacsRepository):
if self.hacs.system.action: if self.hacs.system.action:
await self.hacs.validation.async_run_repository_checks(self) await self.hacs.validation.async_run_repository_checks(self)
async def async_post_uninstall(self) -> None:
"""Run post uninstall steps."""
await self._reload_custom_templates()
async def _reload_custom_templates(self) -> None:
"""Reload custom templates."""
self.logger.debug("%s Reloading custom templates", self.string)
try:
await self.hacs.hass.services.async_call("homeassistant", "reload_custom_templates", {})
except HomeAssistantError as exception:
self.logger.exception("%s %s", self.string, exception)
@concurrent(concurrenttasks=10, backoff_time=5) @concurrent(concurrenttasks=10, backoff_time=5)
async def update_repository(self, ignore_issues=False, force=False): async def update_repository(self, ignore_issues=False, force=False):
"""Update.""" """Update."""
@ -78,7 +93,7 @@ class HacsTemplateRepository(HacsRepository):
self.data.file_name = self.repository_manifest.filename self.data.file_name = self.repository_manifest.filename
self.content.path.local = self.localpath self.content.path.local = self.localpath
# Signal entities to refresh # Signal frontend to refresh
if self.data.installed: if self.data.installed:
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,

View File

@ -1,8 +1,11 @@
"""Class for themes in HACS.""" """Class for themes in HACS."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from homeassistant.exceptions import HomeAssistantError
from ..enums import HacsCategory, HacsDispatchEvent from ..enums import HacsCategory, HacsDispatchEvent
from ..exceptions import HacsException from ..exceptions import HacsException
from ..utils.decorator import concurrent from ..utils.decorator import concurrent
@ -32,12 +35,7 @@ class HacsThemeRepository(HacsRepository):
async def async_post_installation(self): async def async_post_installation(self):
"""Run post installation steps.""" """Run post installation steps."""
try: await self._reload_frontend_themes()
await self.hacs.hass.services.async_call("frontend", "reload_themes", {})
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
pass
self.hacs.async_setup_frontend_endpoint_themes()
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
@ -74,6 +72,18 @@ class HacsThemeRepository(HacsRepository):
if self.hacs.system.action: if self.hacs.system.action:
await self.hacs.validation.async_run_repository_checks(self) await self.hacs.validation.async_run_repository_checks(self)
async def _reload_frontend_themes(self) -> None:
"""Reload frontend themes."""
self.logger.debug("%s Reloading frontend themes", self.string)
try:
await self.hacs.hass.services.async_call("frontend", "reload_themes", {})
except HomeAssistantError as exception:
self.logger.exception("%s %s", self.string, exception)
async def async_post_uninstall(self) -> None:
"""Run post uninstall steps."""
await self._reload_frontend_themes()
@concurrent(concurrenttasks=10, backoff_time=5) @concurrent(concurrenttasks=10, backoff_time=5)
async def update_repository(self, ignore_issues=False, force=False): async def update_repository(self, ignore_issues=False, force=False):
"""Update.""" """Update."""
@ -88,7 +98,7 @@ class HacsThemeRepository(HacsRepository):
self.update_filenames() self.update_filenames()
self.content.path.local = self.localpath self.content.path.local = self.localpath
# Signal entities to refresh # Signal frontend to refresh
if self.data.installed: if self.data.installed:
self.hacs.async_dispatch( self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,

View File

@ -1,4 +1,7 @@
"""Provide info to system health.""" """Provide info to system health."""
from typing import Any
from aiogithubapi.common.const import BASE_API_URL from aiogithubapi.common.const import BASE_API_URL
from homeassistant.components import system_health from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -17,8 +20,11 @@ def async_register(hass: HomeAssistant, register: system_health.SystemHealthRegi
register.async_register_info(system_health_info, "/hacs") register.async_register_info(system_health_info, "/hacs")
async def system_health_info(hass): async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"""Get info for the info page.""" """Get info for the info page."""
if DOMAIN not in hass.data:
return {"Disabled": "HACS is not loaded, but HA still requests this information..."}
hacs: HacsBase = hass.data[DOMAIN] hacs: HacsBase = hass.data[DOMAIN]
response = await hacs.githubapi.rate_limit() response = await hacs.githubapi.rate_limit()
@ -30,6 +36,9 @@ async def system_health_info(hass):
"GitHub Web": system_health.async_check_can_reach_url( "GitHub Web": system_health.async_check_can_reach_url(
hass, "https://github.com/", GITHUB_STATUS hass, "https://github.com/", GITHUB_STATUS
), ),
"HACS Data": system_health.async_check_can_reach_url(
hass, "https://data-v2.hacs.xyz/data.json", CLOUDFLARE_STATUS
),
"GitHub API Calls Remaining": response.data.resources.core.remaining, "GitHub API Calls Remaining": response.data.resources.core.remaining,
"Installed Version": hacs.version, "Installed Version": hacs.version,
"Stage": hacs.stage, "Stage": hacs.stage,
@ -40,9 +49,4 @@ async def system_health_info(hass):
if hacs.system.disabled: if hacs.system.disabled:
data["Disabled"] = hacs.system.disabled_reason data["Disabled"] = hacs.system.disabled_reason
if hacs.configuration.experimental:
data["HACS Data"] = system_health.async_check_can_reach_url(
hass, "https://data-v2.hacs.xyz/data.json", CLOUDFLARE_STATUS
)
return data return data

View File

@ -17,8 +17,7 @@
"acc_logs": "I know how to access Home Assistant logs", "acc_logs": "I know how to access Home Assistant logs",
"acc_addons": "I know that there are no add-ons in HACS", "acc_addons": "I know that there are no add-ons in HACS",
"acc_untested": "I know that everything inside HACS including HACS itself is custom and untested by Home Assistant", "acc_untested": "I know that everything inside HACS including HACS itself is custom and untested by Home Assistant",
"acc_disable": "I know that if I get issues with Home Assistant I should disable all my custom_components", "acc_disable": "I know that if I get issues with Home Assistant I should disable all my custom_components"
"experimental": "Enable experimental features, this is what eventually will become HACS 2.0.0, if you enable it now you do not need to do anything when 2.0.0 is released"
}, },
"description": "Before you can setup HACS you need to acknowledge the following" "description": "Before you can setup HACS you need to acknowledge the following"
}, },
@ -31,7 +30,7 @@
} }
}, },
"progress": { "progress": {
"wait_for_device": "1. Open {url} \n2. Paste the following key to authorize HACS: \n```\n{code}\n```\n" "wait_for_device": "1. Open {url} \n2. Paste the following key to authorize HACS: \n```\n{code}\n```"
} }
}, },
"options": { "options": {
@ -45,11 +44,9 @@
"data": { "data": {
"not_in_use": "Not in use with YAML", "not_in_use": "Not in use with YAML",
"country": "Filter with country code", "country": "Filter with country code",
"experimental": "Enable experimental features",
"release_limit": "Number of releases to show", "release_limit": "Number of releases to show",
"debug": "Enable debug", "debug": "Enable debug",
"appdaemon": "Enable AppDaemon apps discovery & tracking", "appdaemon": "Enable AppDaemon apps discovery & tracking",
"netdaemon": "[DEPRECATED] Enable NetDaemon apps discovery & tracking",
"sidepanel_icon": "Side panel icon", "sidepanel_icon": "Side panel icon",
"sidepanel_title": "Side panel title" "sidepanel_title": "Side panel title"
} }
@ -71,10 +68,17 @@
"removed": { "removed": {
"title": "Repository removed from HACS", "title": "Repository removed from HACS",
"description": "Because {reason}, `{name}` has been removed from HACS. Please visit the [HACS Panel](/hacs/repository/{repositry_id}) to remove it." "description": "Because {reason}, `{name}` has been removed from HACS. Please visit the [HACS Panel](/hacs/repository/{repositry_id}) to remove it."
}
}, },
"deprecated_yaml_configuration": { "entity": {
"title": "YAML configuration is deprecated", "switch": {
"description": "YAML configuration of HACS is deprecated and will be removed in version 2.0.0, there will be no automatic import of this.\nPlease remove it from your configuration, restart Home Assistant and use the UI to configure it instead." "pre-release": {
"name": "Pre-release",
"state": {
"off": "No pre-releases",
"on": "Pre-releases preferred"
}
}
} }
} }
} }

View File

@ -1,22 +1,28 @@
"""Update entities for HACS.""" """Update entities for HACS."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.update import UpdateEntity from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, HomeAssistantError, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .base import HacsBase from .base import HacsBase
from .const import DOMAIN from .const import DOMAIN
from .entity import HacsRepositoryEntity from .entity import HacsRepositoryEntity
from .enums import HacsCategory, HacsDispatchEvent from .enums import HacsCategory, HacsDispatchEvent
from .exceptions import HacsException
async def async_setup_entry(hass, _config_entry, async_add_devices): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Setup update platform.""" """Setup update platform."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data[DOMAIN]
async_add_devices( async_add_entities(
HacsRepositoryUpdateEntity(hacs=hacs, repository=repository) HacsRepositoryUpdateEntity(hacs=hacs, repository=repository)
for repository in hacs.repositories.list_downloaded for repository in hacs.repositories.list_downloaded
) )
@ -25,13 +31,12 @@ async def async_setup_entry(hass, _config_entry, async_add_devices):
class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity): class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
"""Update entities for repositories downloaded with HACS.""" """Update entities for repositories downloaded with HACS."""
@property _attr_supported_features = (
def supported_features(self) -> int | None: UpdateEntityFeature.INSTALL
"""Return the supported features of the entity.""" | UpdateEntityFeature.SPECIFIC_VERSION
features = 4 | 16 | UpdateEntityFeature.PROGRESS
if self.repository.can_download: | UpdateEntityFeature.RELEASE_NOTES
features = features | 1 )
return features
@property @property
def name(self) -> str | None: def name(self) -> str | None:
@ -58,8 +63,6 @@ class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
@property @property
def release_summary(self) -> str | None: def release_summary(self) -> str | None:
"""Return the release summary.""" """Return the release summary."""
if not self.repository.can_download:
return f"<ha-alert alert-type='warning'>Requires Home Assistant {self.repository.repository_manifest.homeassistant}</ha-alert>"
if self.repository.pending_restart: if self.repository.pending_restart:
return "<ha-alert alert-type='error'>Restart of Home Assistant required</ha-alert>" return "<ha-alert alert-type='error'>Restart of Home Assistant required</ha-alert>"
return None return None
@ -77,17 +80,18 @@ class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
async def async_install(self, version: str | None, backup: bool, **kwargs: Any) -> None: async def async_install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
"""Install an update.""" """Install an update."""
if self.repository.display_version_or_commit == "version": to_download = version or self.latest_version
self._update_in_progress(progress=10) if to_download == self.installed_version:
self.repository.data.selected_tag = self.latest_version raise HomeAssistantError(f"Version {self.installed_version} of {
await self.repository.update_repository(force=True) self.repository.data.full_name} is already downloaded")
self._update_in_progress(progress=20) try:
await self.repository.async_install() await self.repository.async_download_repository(ref=version or self.latest_version)
self._update_in_progress(progress=False) except HacsException as exception:
raise HomeAssistantError(exception) from exception
async def async_release_notes(self) -> str | None: async def async_release_notes(self) -> str | None:
"""Return the release notes.""" """Return the release notes."""
if self.repository.pending_restart or not self.repository.can_download: if self.repository.pending_restart:
return None return None
if self.latest_version not in self.repository.data.published_tags: if self.latest_version not in self.repository.data.published_tags:
@ -102,9 +106,18 @@ class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
self.repository.data.last_version = next(iter(self.repository.data.published_tags)) self.repository.data.last_version = next(iter(self.repository.data.published_tags))
release_notes = "" release_notes = ""
if len(self.repository.releases.objects) > 0: # Compile release notes from installed version up to the latest
release = self.repository.releases.objects[0] if self.installed_version in self.repository.data.published_tags:
release_notes += release.body for release in self.repository.releases.objects:
if release.tag_name == self.installed_version:
break
release_notes += f"# {release.tag_name}"
if release.tag_name != release.name:
release_notes += f" - {release.name}"
release_notes += f"\n\n{release.body}"
release_notes += "\n\n---\n\n"
elif any(self.repository.releases.objects):
release_notes += self.repository.releases.objects[0].body
if self.repository.pending_update: if self.repository.pending_update:
if self.repository.data.category == HacsCategory.INTEGRATION: if self.repository.data.category == HacsCategory.INTEGRATION:

View File

@ -1,4 +1,5 @@
"""Backup.""" """Backup."""
from __future__ import annotations from __future__ import annotations
import os import os
@ -27,7 +28,7 @@ class Backup:
backup_path: str = DEFAULT_BACKUP_PATH, backup_path: str = DEFAULT_BACKUP_PATH,
repository: HacsRepository | None = None, repository: HacsRepository | None = None,
) -> None: ) -> None:
"""initialize.""" """Initialize."""
self.hacs = hacs self.hacs = hacs
self.repository = repository self.repository = repository
self.local_path = local_path or repository.content.path.local self.local_path = local_path or repository.content.path.local
@ -107,33 +108,3 @@ class Backup:
while os.path.exists(self.backup_path): while os.path.exists(self.backup_path):
sleep(0.1) sleep(0.1)
self.hacs.log.debug("Backup dir %s cleared", self.backup_path) self.hacs.log.debug("Backup dir %s cleared", self.backup_path)
class BackupNetDaemon(Backup):
"""BackupNetDaemon."""
def create(self) -> None:
"""Create a backup in /tmp"""
if not self._init_backup_dir():
return
for filename in os.listdir(self.repository.content.path.local):
if not filename.endswith(".yaml"):
continue
source_file_name = f"{self.repository.content.path.local}/{filename}"
target_file_name = f"{self.backup_path}/{filename}"
shutil.copyfile(source_file_name, target_file_name)
def restore(self) -> None:
"""Create a backup in /tmp"""
if not os.path.exists(self.backup_path):
return
for filename in os.listdir(self.backup_path):
if not filename.endswith(".yaml"):
continue
source_file_name = f"{self.backup_path}/{filename}"
target_file_name = f"{self.repository.content.path.local}/{filename}"
shutil.copyfile(source_file_name, target_file_name)

View File

@ -1,74 +1,9 @@
"""HACS Configuration Schemas.""" """HACS Configuration Schemas."""
# pylint: disable=dangerous-default-value
import voluptuous as vol
from ..const import LOCALE
# Configuration: # Configuration:
TOKEN = "token"
SIDEPANEL_TITLE = "sidepanel_title" SIDEPANEL_TITLE = "sidepanel_title"
SIDEPANEL_ICON = "sidepanel_icon" SIDEPANEL_ICON = "sidepanel_icon"
FRONTEND_REPO = "frontend_repo"
FRONTEND_REPO_URL = "frontend_repo_url"
APPDAEMON = "appdaemon" APPDAEMON = "appdaemon"
NETDAEMON = "netdaemon"
# Options: # Options:
COUNTRY = "country" COUNTRY = "country"
DEBUG = "debug"
RELEASE_LIMIT = "release_limit"
EXPERIMENTAL = "experimental"
# Config group
PATH_OR_URL = "frontend_repo_path_or_url"
def hacs_base_config_schema(config: dict = {}) -> dict:
"""Return a shcema configuration dict for HACS."""
if not config:
config = {
TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
}
return {
vol.Required(TOKEN, default=config.get(TOKEN)): str,
}
def hacs_config_option_schema(options: dict = {}) -> dict:
"""Return a shcema for HACS configuration options."""
if not options:
options = {
APPDAEMON: False,
COUNTRY: "ALL",
DEBUG: False,
EXPERIMENTAL: False,
NETDAEMON: False,
RELEASE_LIMIT: 5,
SIDEPANEL_ICON: "hacs:hacs",
SIDEPANEL_TITLE: "HACS",
FRONTEND_REPO: "",
FRONTEND_REPO_URL: "",
}
return {
vol.Optional(SIDEPANEL_TITLE, default=options.get(SIDEPANEL_TITLE)): str,
vol.Optional(SIDEPANEL_ICON, default=options.get(SIDEPANEL_ICON)): str,
vol.Optional(RELEASE_LIMIT, default=options.get(RELEASE_LIMIT)): int,
vol.Optional(COUNTRY, default=options.get(COUNTRY)): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=options.get(APPDAEMON)): bool,
vol.Optional(NETDAEMON, default=options.get(NETDAEMON)): bool,
vol.Optional(DEBUG, default=options.get(DEBUG)): bool,
vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool,
vol.Exclusive(FRONTEND_REPO, PATH_OR_URL): str,
vol.Exclusive(FRONTEND_REPO_URL, PATH_OR_URL): str,
}
def hacs_config_combined() -> dict:
"""Combine the configuration options."""
base = hacs_base_config_schema()
options = hacs_config_option_schema()
for option in options:
base[option] = options[option]
return base

View File

@ -1,13 +1,13 @@
"""Data handler for HACS.""" """Data handler for HACS."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import json as json_util
from ..base import HacsBase from ..base import HacsBase
from ..const import HACS_REPOSITORY_ID from ..const import HACS_REPOSITORY_ID
@ -47,6 +47,7 @@ EXPORTED_DOWNLOADED_REPOSITORY_DATA = EXPORTED_REPOSITORY_DATA + (
("last_version", None), ("last_version", None),
("manifest_name", None), ("manifest_name", None),
("open_issues", 0), ("open_issues", 0),
("prerelease", None),
("published_tags", []), ("published_tags", []),
("releases", False), ("releases", False),
("selected_tag", None), ("selected_tag", None),
@ -84,7 +85,6 @@ class HacsData:
"ignored_repositories": self.hacs.common.ignored_repositories, "ignored_repositories": self.hacs.common.ignored_repositories,
}, },
) )
if self.hacs.configuration.experimental:
await self._async_store_experimental_content_and_repos() await self._async_store_experimental_content_and_repos()
await self._async_store_content_and_repos() await self._async_store_content_and_repos()
@ -100,7 +100,7 @@ class HacsData:
for event in (HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.CONFIG): for event in (HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.CONFIG):
self.hacs.async_dispatch(event, {}) self.hacs.async_dispatch(event, {})
async def _async_store_experimental_content_and_repos(self, _=None): # bb: ignore async def _async_store_experimental_content_and_repos(self, _=None):
"""Store the main repos file and each repo that is out of date.""" """Store the main repos file and each repo that is out of date."""
# Repositories # Repositories
self.content = {} self.content = {}
@ -165,29 +165,16 @@ class HacsData:
pass pass
try: try:
data = ( repositories = await async_load_from_store(self.hacs.hass, "repositories")
await async_load_from_store( if not repositories and (data := await async_load_from_store(self.hacs.hass, "data")):
self.hacs.hass,
"data" if self.hacs.configuration.experimental else "repositories",
)
or {}
)
if data and self.hacs.configuration.experimental:
for category, entries in data.get("repositories", {}).items(): for category, entries in data.get("repositories", {}).items():
for repository in entries: for repository in entries:
repositories[repository["id"]] = {"category": category, **repository} repositories[repository["id"]] = {"category": category, **repository}
else:
repositories = (
data or await async_load_from_store(self.hacs.hass, "repositories") or {}
)
except HomeAssistantError as exception: except HomeAssistantError as exception:
self.hacs.log.error( self.hacs.log.error(
"Could not read %s, restore the file from a backup - %s", "Could not read %s, restore the file from a backup - %s",
self.hacs.hass.config.path( self.hacs.hass.config.path(".storage/hacs.data"),
".storage/hacs.data"
if self.hacs.configuration.experimental
else ".storage/hacs.repositories"
),
exception, exception,
) )
self.hacs.disable_hacs(HacsDisabledReason.RESTORE) self.hacs.disable_hacs(HacsDisabledReason.RESTORE)
@ -196,13 +183,7 @@ class HacsData:
if not hacs and not repositories: if not hacs and not repositories:
# Assume new install # Assume new install
self.hacs.status.new = True self.hacs.status.new = True
if self.hacs.configuration.experimental:
return True return True
self.logger.info("<HacsData restore> Loading base repository information")
repositories = await self.hacs.hass.async_add_executor_job(
json_util.load_json,
f"{self.hacs.core.config_path}/custom_components/hacs/utils/default.repositories",
)
self.logger.info("<HacsData restore> Restore started") self.logger.info("<HacsData restore> Restore started")
@ -242,7 +223,8 @@ class HacsData:
self.logger.info("<HacsData restore> Restore done") self.logger.info("<HacsData restore> Restore done")
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.logger.critical( self.logger.critical(
"<HacsData restore> [%s] Restore Failed!", exception, exc_info=exception "<HacsData restore> [%s] Restore Failed!", exception, exc_info=exception
@ -250,22 +232,28 @@ class HacsData:
return False return False
return True return True
async def register_unknown_repositories(self, repositories, category: str | None = None): async def register_unknown_repositories(
self, repositories: dict[str, dict[str, Any]], category: str | None = None
):
"""Registry any unknown repositories.""" """Registry any unknown repositories."""
register_tasks = [ for repo_idx, (entry, repo_data) in enumerate(repositories.items()):
self.hacs.async_register_repository( # async_register_repository is awaited in a loop
# since its unlikely to ever suspend at startup
if (
entry == "0"
or repo_data.get("category", category) is None
or self.hacs.repositories.is_registered(repository_id=entry)
):
continue
await self.hacs.async_register_repository(
repository_full_name=repo_data["full_name"], repository_full_name=repo_data["full_name"],
category=repo_data.get("category", category), category=repo_data.get("category", category),
check=False, check=False,
repository_id=entry, repository_id=entry,
) )
for entry, repo_data in repositories.items() if repo_idx % 100 == 0:
if entry != "0" # yield to avoid blocking the event loop
and not self.hacs.repositories.is_registered(repository_id=entry) await asyncio.sleep(0)
and repo_data.get("category", category) is not None
]
if register_tasks:
await asyncio.gather(*register_tasks)
@callback @callback
def async_restore_repository(self, entry: str, repository_data: dict[str, Any]): def async_restore_repository(self, entry: str, repository_data: dict[str, Any]):
@ -278,8 +266,13 @@ class HacsData:
if not repository: if not repository:
return return
# Restore repository attributes try:
self.hacs.repositories.set_repository_id(repository, entry) self.hacs.repositories.set_repository_id(repository, entry)
except ValueError as exception:
self.logger.warning("<HacsData async_restore_repository> duplicate IDs %s", exception)
return
# Restore repository attributes
repository.data.authors = repository_data.get("authors", []) repository.data.authors = repository_data.get("authors", [])
repository.data.description = repository_data.get("description", "") repository.data.description = repository_data.get("description", "")
repository.data.downloads = repository_data.get("downloads", 0) repository.data.downloads = repository_data.get("downloads", 0)
@ -302,18 +295,22 @@ class HacsData:
repository.data.selected_tag = repository_data.get("selected_tag") repository.data.selected_tag = repository_data.get("selected_tag")
repository.data.show_beta = repository_data.get("show_beta", False) repository.data.show_beta = repository_data.get("show_beta", False)
repository.data.last_version = repository_data.get("last_version") repository.data.last_version = repository_data.get("last_version")
repository.data.prerelease = repository_data.get("prerelease")
repository.data.last_commit = repository_data.get("last_commit") repository.data.last_commit = repository_data.get("last_commit")
repository.data.installed_version = repository_data.get("version_installed") repository.data.installed_version = repository_data.get("version_installed")
repository.data.installed_commit = repository_data.get("installed_commit") repository.data.installed_commit = repository_data.get("installed_commit")
repository.data.manifest_name = repository_data.get("manifest_name") repository.data.manifest_name = repository_data.get("manifest_name")
if last_fetched := repository_data.get("last_fetched"): if last_fetched := repository_data.get("last_fetched"):
repository.data.last_fetched = datetime.fromtimestamp(last_fetched) repository.data.last_fetched = datetime.fromtimestamp(last_fetched, UTC)
repository.repository_manifest = HacsManifest.from_dict( repository.repository_manifest = HacsManifest.from_dict(
repository_data.get("manifest") or repository_data.get("repository_manifest") or {} repository_data.get("manifest") or repository_data.get("repository_manifest") or {}
) )
if repository.data.prerelease == repository.data.last_version:
repository.data.prerelease = None
if repository.localpath is not None and is_safe(self.hacs, repository.localpath): if repository.localpath is not None and is_safe(self.hacs, repository.localpath):
# Set local path # Set local path
repository.content.path.local = repository.localpath repository.content.path.local = repository.localpath

View File

@ -1,4 +1,5 @@
"""Util to decode content from the github API.""" """Util to decode content from the github API."""
from base64 import b64decode from base64 import b64decode

View File

@ -1,9 +1,11 @@
"""HACS Decorators.""" """HACS Decorators."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Coroutine
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING, Any, Coroutine from typing import TYPE_CHECKING, Any
from ..const import DEFAULT_CONCURRENT_BACKOFF_TIME, DEFAULT_CONCURRENT_TASKS from ..const import DEFAULT_CONCURRENT_BACKOFF_TIME, DEFAULT_CONCURRENT_TASKS

View File

@ -1,4 +1,5 @@
"""Filter functions.""" """Filter functions."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -1,10 +1,5 @@
"""JSON utils.""" """JSON utils."""
try: from homeassistant.util.json import json_loads
# Could be removed after 2022.06 is the min version
# But in case Home Assistant changes, keep this try/except here...
from homeassistant.helpers.json import json_loads
except ImportError:
from json import loads as json_loads
__all__ = ["json_loads"] __all__ = ["json_loads"]

View File

@ -1,4 +1,5 @@
"""Custom logger for HACS.""" """Custom logger for HACS."""
import logging import logging
from ..const import PACKAGE_NAME from ..const import PACKAGE_NAME

View File

@ -1,6 +1,8 @@
"""Path utils""" """Path utils"""
from __future__ import annotations from __future__ import annotations
from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -8,14 +10,32 @@ if TYPE_CHECKING:
from ..base import HacsBase from ..base import HacsBase
@lru_cache(maxsize=1)
def _get_safe_paths(
config_path: str,
appdaemon_path: str,
plugin_path: str,
python_script_path: str,
theme_path: str,
) -> set[str]:
"""Get safe paths."""
return {
Path(f"{config_path}/{appdaemon_path}").as_posix(),
Path(f"{config_path}/{plugin_path}").as_posix(),
Path(f"{config_path}/{python_script_path}").as_posix(),
Path(f"{config_path}/{theme_path}").as_posix(),
Path(f"{config_path}/custom_components/").as_posix(),
Path(f"{config_path}/custom_templates/").as_posix(),
}
def is_safe(hacs: HacsBase, path: str | Path) -> bool: def is_safe(hacs: HacsBase, path: str | Path) -> bool:
"""Helper to check if path is safe to remove.""" """Helper to check if path is safe to remove."""
return Path(path).as_posix() not in ( configuration = hacs.configuration
Path(f"{hacs.core.config_path}/{hacs.configuration.appdaemon_path}").as_posix(), return Path(path).as_posix() not in _get_safe_paths(
Path(f"{hacs.core.config_path}/{hacs.configuration.netdaemon_path}").as_posix(), hacs.core.config_path,
Path(f"{hacs.core.config_path}/{hacs.configuration.plugin_path}").as_posix(), configuration.appdaemon_path,
Path(f"{hacs.core.config_path}/{hacs.configuration.python_script_path}").as_posix(), configuration.plugin_path,
Path(f"{hacs.core.config_path}/{hacs.configuration.theme_path}").as_posix(), configuration.python_script_path,
Path(f"{hacs.core.config_path}/custom_components/").as_posix(), configuration.theme_path,
Path(f"{hacs.core.config_path}/custom_templates/").as_posix(),
) )

View File

@ -1,9 +1,10 @@
"""The QueueManager class.""" """The QueueManager class."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Coroutine
import time import time
from typing import Coroutine
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -60,9 +61,6 @@ class QueueManager:
for task in self.queue: for task in self.queue:
local_queue.append(task) local_queue.append(task)
for task in local_queue:
self.queue.remove(task)
_LOGGER.debug("<QueueManager> Starting queue execution for %s tasks", len(local_queue)) _LOGGER.debug("<QueueManager> Starting queue execution for %s tasks", len(local_queue))
start = time.time() start = time.time()
result = await asyncio.gather(*local_queue, return_exceptions=True) result = await asyncio.gather(*local_queue, return_exceptions=True)
@ -71,6 +69,9 @@ class QueueManager:
_LOGGER.error("<QueueManager> %s", entry) _LOGGER.error("<QueueManager> %s", entry)
end = time.time() - start end = time.time() - start
for task in local_queue:
self.queue.remove(task)
_LOGGER.debug( _LOGGER.debug(
"<QueueManager> Queue execution finished for %s tasks finished in %.2f seconds", "<QueueManager> Queue execution finished for %s tasks finished in %.2f seconds",
len(local_queue), len(local_queue),

View File

@ -1,4 +1,5 @@
"""Regex utils""" """Regex utils"""
from __future__ import annotations from __future__ import annotations
import re import re

View File

@ -1,4 +1,5 @@
"""Storage handers.""" """Storage handers."""
from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.util import json as json_util from homeassistant.util import json as json_util

View File

@ -1,7 +1,10 @@
"""Validation utilities.""" """Validation utilities."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from homeassistant.helpers.config_validation import url as url_validator from homeassistant.helpers.config_validation import url as url_validator
@ -45,9 +48,9 @@ HACS_MANIFEST_JSON_SCHEMA = vol.Schema(
vol.Optional("content_in_root"): bool, vol.Optional("content_in_root"): bool,
vol.Optional("country"): _country_validator, vol.Optional("country"): _country_validator,
vol.Optional("filename"): str, vol.Optional("filename"): str,
vol.Optional("hacs"): vol.Coerce(AwesomeVersion), vol.Optional("hacs"): str,
vol.Optional("hide_default_branch"): bool, vol.Optional("hide_default_branch"): bool,
vol.Optional("homeassistant"): vol.Coerce(AwesomeVersion), vol.Optional("homeassistant"): str,
vol.Optional("persistent_directory"): str, vol.Optional("persistent_directory"): str,
vol.Optional("render_readme"): bool, vol.Optional("render_readme"): bool,
vol.Optional("zip_release"): bool, vol.Optional("zip_release"): bool,
@ -67,3 +70,146 @@ INTEGRATION_MANIFEST_JSON_SCHEMA = vol.Schema(
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
def validate_repo_data(schema: dict[str, Any], extra: int) -> Callable[[Any], Any]:
"""Return a validator for repo data.
This is used instead of vol.All to always try both the repo schema and
and the validate_version validator.
"""
_schema = vol.Schema(schema, extra=extra)
def validate_repo_data(data: Any) -> Any:
"""Validate integration repo data."""
schema_errors: vol.MultipleInvalid | None = None
try:
_schema(data)
except vol.MultipleInvalid as err:
schema_errors = err
try:
validate_version(data)
except vol.Invalid as err:
if schema_errors:
schema_errors.add(err)
else:
raise
if schema_errors:
raise schema_errors
return data
return validate_repo_data
def validate_version(data: Any) -> Any:
"""Ensure at least one of last_commit or last_version is present."""
if "last_commit" not in data and "last_version" not in data:
raise vol.Invalid("Expected at least one of [`last_commit`, `last_version`], got none")
return data
V2_COMMON_DATA_JSON_SCHEMA = {
vol.Required("description"): vol.Any(str, None),
vol.Optional("downloads"): int,
vol.Optional("etag_releases"): str,
vol.Required("etag_repository"): str,
vol.Required("full_name"): str,
vol.Optional("last_commit"): str,
vol.Required("last_fetched"): vol.Any(int, float),
vol.Required("last_updated"): str,
vol.Optional("last_version"): str,
vol.Optional("prerelease"): str,
vol.Required("manifest"): {
vol.Optional("country"): vol.Any([str], False),
vol.Optional("name"): str,
},
vol.Optional("open_issues"): int,
vol.Optional("stargazers_count"): int,
vol.Optional("topics"): [str],
}
V2_INTEGRATION_DATA_JSON_SCHEMA = {
**V2_COMMON_DATA_JSON_SCHEMA,
vol.Required("domain"): str,
vol.Required("manifest_name"): str,
}
_V2_REPO_SCHEMAS = {
"appdaemon": V2_COMMON_DATA_JSON_SCHEMA,
"integration": V2_INTEGRATION_DATA_JSON_SCHEMA,
"plugin": V2_COMMON_DATA_JSON_SCHEMA,
"python_script": V2_COMMON_DATA_JSON_SCHEMA,
"template": V2_COMMON_DATA_JSON_SCHEMA,
"theme": V2_COMMON_DATA_JSON_SCHEMA,
}
# Used when validating repos in the hacs integration, discards extra keys
VALIDATE_FETCHED_V2_REPO_DATA = {
category: validate_repo_data(schema, vol.REMOVE_EXTRA)
for category, schema in _V2_REPO_SCHEMAS.items()
}
# Used when validating repos when generating data, fails on extra keys
VALIDATE_GENERATED_V2_REPO_DATA = {
category: vol.Schema({str: validate_repo_data(schema, vol.PREVENT_EXTRA)})
for category, schema in _V2_REPO_SCHEMAS.items()
}
V2_CRITICAL_REPO_DATA_SCHEMA = {
vol.Required("link"): str,
vol.Required("reason"): str,
vol.Required("repository"): str,
}
# Used when validating critical repos in the hacs integration, discards extra keys
VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA = vol.Schema(
V2_CRITICAL_REPO_DATA_SCHEMA,
extra=vol.REMOVE_EXTRA,
)
# Used when validating critical repos when generating data, fails on extra keys
VALIDATE_GENERATED_V2_CRITICAL_REPO_SCHEMA = vol.Schema(
[
vol.Schema(
V2_CRITICAL_REPO_DATA_SCHEMA,
extra=vol.PREVENT_EXTRA,
)
]
)
V2_REMOVED_REPO_DATA_SCHEMA = {
vol.Optional("link"): str,
vol.Optional("reason"): str,
vol.Required("removal_type"): vol.In(
[
"Integration is missing a version, and is abandoned.",
"Remove",
"archived",
"blacklist",
"critical",
"deprecated",
"removal",
"remove",
"removed",
"replaced",
"repository",
]
),
vol.Required("repository"): str,
}
# Used when validating removed repos in the hacs integration, discards extra keys
VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA = vol.Schema(
V2_REMOVED_REPO_DATA_SCHEMA,
extra=vol.REMOVE_EXTRA,
)
# Used when validating removed repos when generating data, fails on extra keys
VALIDATE_GENERATED_V2_REMOVED_REPO_SCHEMA = vol.Schema(
[
vol.Schema(
V2_REMOVED_REPO_DATA_SCHEMA,
extra=vol.PREVENT_EXTRA,
)
]
)

View File

@ -1,4 +1,5 @@
"""Version utils.""" """Version utils."""
from __future__ import annotations from __future__ import annotations
from functools import lru_cache from functools import lru_cache

View File

@ -1,7 +1,37 @@
"""Workarounds for issues that should not be fixed.""" """Workarounds."""
from homeassistant.core import HomeAssistant
DOMAIN_OVERRIDES = { DOMAIN_OVERRIDES = {
# https://github.com/hacs/integration/issues/2465 # https://github.com/hacs/integration/issues/2465
"custom-components/sensor.custom_aftership": "custom_aftership" "custom-components/sensor.custom_aftership": "custom_aftership"
} }
try:
from homeassistant.components.http import StaticPathConfig
async def async_register_static_path(
hass: HomeAssistant,
url_path: str,
path: str,
cache_headers: bool = True,
) -> None:
"""Register a static path with the HTTP component."""
await hass.http.async_register_static_paths(
[StaticPathConfig(url_path, path, cache_headers)]
)
except ImportError:
async def async_register_static_path(
hass: HomeAssistant,
url_path: str,
path: str,
cache_headers: bool = True,
) -> None:
"""Register a static path with the HTTP component.
Legacy: Can be removed when min version is 2024.7
https://developers.home-assistant.io/blog/2024/06/18/async_register_static_paths/
"""
hass.http.register_static_path(url_path, path, cache_headers)

View File

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
from ..repositories.base import HacsRepository from typing import TYPE_CHECKING
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
async def async_setup_validator(repository: HacsRepository) -> Validator: async def async_setup_validator(repository: HacsRepository) -> Validator:
"""Set up this validator.""" """Set up this validator."""
@ -15,7 +19,7 @@ class Validator(ActionValidationBase):
more_info = "https://hacs.xyz/docs/publish/include#check-archived" more_info = "https://hacs.xyz/docs/publish/include#check-archived"
allow_fork = False allow_fork = False
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
if self.repository.data.archived: if self.repository.data.archived:
raise ValidationException("The repository is archived") raise ValidationException("The repository is archived")

View File

@ -1,12 +1,13 @@
"""Base class for validation.""" """Base class for validation."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from ..enums import HacsCategory
from ..exceptions import HacsException from ..exceptions import HacsException
if TYPE_CHECKING: if TYPE_CHECKING:
from ..enums import HacsCategory
from ..repositories.base import HacsRepository from ..repositories.base import HacsRepository
@ -17,7 +18,7 @@ class ValidationException(HacsException):
class ActionValidationBase: class ActionValidationBase:
"""Base class for action validation.""" """Base class for action validation."""
categories: list[HacsCategory] = [] categories: tuple[HacsCategory, ...] = ()
allow_fork: bool = True allow_fork: bool = True
more_info: str = "https://hacs.xyz/docs/publish/action" more_info: str = "https://hacs.xyz/docs/publish/action"
@ -34,7 +35,7 @@ class ActionValidationBase:
async def async_validate(self) -> None: async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
async def execute_validation(self, *_, **__) -> None: async def execute_validation(self, *_: Any, **__: Any) -> None:
"""Execute the task defined in subclass.""" """Execute the task defined in subclass."""
self.failed = False self.failed = False

View File

@ -1,10 +1,14 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from custom_components.hacs.enums import HacsCategory from custom_components.hacs.enums import HacsCategory
from ..repositories.base import HacsRepository
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
URL = "https://brands.home-assistant.io/domains.json" URL = "https://brands.home-assistant.io/domains.json"
@ -17,9 +21,9 @@ class Validator(ActionValidationBase):
"""Validate the repository.""" """Validate the repository."""
more_info = "https://hacs.xyz/docs/publish/include#check-brands" more_info = "https://hacs.xyz/docs/publish/include#check-brands"
categories = [HacsCategory.INTEGRATION] categories = (HacsCategory.INTEGRATION,)
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
response = await self.hacs.session.get(URL) response = await self.hacs.session.get(URL)

View File

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
from ..repositories.base import HacsRepository from typing import TYPE_CHECKING
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
async def async_setup_validator(repository: HacsRepository) -> Validator: async def async_setup_validator(repository: HacsRepository) -> Validator:
"""Set up this validator.""" """Set up this validator."""
@ -15,7 +19,7 @@ class Validator(ActionValidationBase):
more_info = "https://hacs.xyz/docs/publish/include#check-repository" more_info = "https://hacs.xyz/docs/publish/include#check-repository"
allow_fork = False allow_fork = False
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
if not self.repository.data.description: if not self.repository.data.description:
raise ValidationException("The repository has no description") raise ValidationException("The repository has no description")

View File

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from voluptuous.error import Invalid from voluptuous.error import Invalid
from voluptuous.humanize import humanize_error
from ..enums import RepositoryFile from ..enums import HacsCategory, RepositoryFile
from ..repositories.base import HacsRepository from ..repositories.base import HacsManifest, HacsRepository
from ..utils.validate import HACS_MANIFEST_JSON_SCHEMA from ..utils.validate import HACS_MANIFEST_JSON_SCHEMA
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
@ -18,13 +19,17 @@ class Validator(ActionValidationBase):
more_info = "https://hacs.xyz/docs/publish/include#check-hacs-manifest" more_info = "https://hacs.xyz/docs/publish/include#check-hacs-manifest"
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
if RepositoryFile.HACS_JSON not in [x.filename for x in self.repository.tree]: if RepositoryFile.HACS_JSON not in [x.filename for x in self.repository.tree]:
raise ValidationException(f"The repository has no '{RepositoryFile.HACS_JSON}' file") raise ValidationException(f"The repository has no '{RepositoryFile.HACS_JSON}' file")
content = await self.repository.async_get_hacs_json(self.repository.ref) content = await self.repository.async_get_hacs_json(self.repository.ref)
try: try:
HACS_MANIFEST_JSON_SCHEMA(content) hacsjson = HacsManifest.from_dict(HACS_MANIFEST_JSON_SCHEMA(content))
except Invalid as exception: except Invalid as exception:
raise ValidationException(exception) from exception raise ValidationException(humanize_error(content, exception)) from exception
if self.repository.data.category == HacsCategory.INTEGRATION:
if hacsjson.zip_release and not hacsjson.filename:
raise ValidationException("zip_release is True, but filename is not set")

View File

@ -1,9 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from ..enums import HacsCategory from ..enums import HacsCategory
from ..repositories.base import HacsRepository
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
IGNORED = ["-shield", "img.shields.io", "buymeacoffee.com"] IGNORED = ["-shield", "img.shields.io", "buymeacoffee.com"]
@ -15,12 +19,12 @@ async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator(ActionValidationBase): class Validator(ActionValidationBase):
"""Validate the repository.""" """Validate the repository."""
categories = [HacsCategory.PLUGIN, HacsCategory.THEME] categories = (HacsCategory.PLUGIN, HacsCategory.THEME)
more_info = "https://hacs.xyz/docs/publish/include#check-images" more_info = "https://hacs.xyz/docs/publish/include#check-images"
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
info = await self.repository.async_get_info_file_contents() info = await self.repository.async_get_info_file_contents(version=self.repository.ref)
for line in info.split("\n"): for line in info.split("\n"):
if "<img" in line or "![" in line: if "<img" in line or "![" in line:
if [ignore for ignore in IGNORED if ignore in line]: if [ignore for ignore in IGNORED if ignore in line]:

View File

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
from ..repositories.base import HacsRepository from typing import TYPE_CHECKING
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
async def async_setup_validator(repository: HacsRepository) -> Validator: async def async_setup_validator(repository: HacsRepository) -> Validator:
"""Set up this validator.""" """Set up this validator."""
@ -14,7 +18,7 @@ class Validator(ActionValidationBase):
more_info = "https://hacs.xyz/docs/publish/include#check-info" more_info = "https://hacs.xyz/docs/publish/include#check-info"
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
filenames = [x.filename.lower() for x in self.repository.tree] filenames = [x.filename.lower() for x in self.repository.tree]
if "readme" in filenames: if "readme" in filenames:

View File

@ -1,13 +1,17 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from voluptuous.error import Invalid from voluptuous.error import Invalid
from ..enums import HacsCategory, RepositoryFile from ..enums import HacsCategory, RepositoryFile
from ..repositories.base import HacsRepository
from ..repositories.integration import HacsIntegrationRepository
from ..utils.validate import INTEGRATION_MANIFEST_JSON_SCHEMA from ..utils.validate import INTEGRATION_MANIFEST_JSON_SCHEMA
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
from ..repositories.integration import HacsIntegrationRepository
async def async_setup_validator(repository: HacsRepository) -> Validator: async def async_setup_validator(repository: HacsRepository) -> Validator:
"""Set up this validator.""" """Set up this validator."""
@ -19,16 +23,16 @@ class Validator(ActionValidationBase):
repository: HacsIntegrationRepository repository: HacsIntegrationRepository
more_info = "https://hacs.xyz/docs/publish/include#check-manifest" more_info = "https://hacs.xyz/docs/publish/include#check-manifest"
categories = [HacsCategory.INTEGRATION] categories = (HacsCategory.INTEGRATION,)
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
if RepositoryFile.MAINIFEST_JSON not in [x.filename for x in self.repository.tree]: if RepositoryFile.MAINIFEST_JSON not in [x.filename for x in self.repository.tree]:
raise ValidationException( raise ValidationException(
f"The repository has no '{RepositoryFile.MAINIFEST_JSON}' file" f"The repository has no '{RepositoryFile.MAINIFEST_JSON}' file"
) )
content = await self.repository.async_get_integration_manifest(self.repository.ref) content = await self.repository.get_integration_manifest(version=self.repository.ref)
try: try:
INTEGRATION_MANIFEST_JSON_SCHEMA(content) INTEGRATION_MANIFEST_JSON_SCHEMA(content)
except Invalid as exception: except Invalid as exception:

View File

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
from ..repositories.base import HacsRepository from typing import TYPE_CHECKING
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
async def async_setup_validator(repository: HacsRepository) -> Validator: async def async_setup_validator(repository: HacsRepository) -> Validator:
"""Set up this validator.""" """Set up this validator."""
@ -15,7 +19,7 @@ class Validator(ActionValidationBase):
more_info = "https://hacs.xyz/docs/publish/include#check-repository" more_info = "https://hacs.xyz/docs/publish/include#check-repository"
allow_fork = False allow_fork = False
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
if not self.repository.data.has_issues: if not self.repository.data.has_issues:
raise ValidationException("The repository does not have issues enabled") raise ValidationException("The repository does not have issues enabled")

View File

@ -1,4 +1,5 @@
"""Hacs validation manager.""" """Hacs validation manager."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -7,13 +8,12 @@ import os
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant
from ..repositories.base import HacsRepository
from .base import ActionValidationBase
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from ..base import HacsBase from ..base import HacsBase
from ..repositories.base import HacsRepository
from .base import ActionValidationBase
class ValidationManager: class ValidationManager:
@ -23,16 +23,16 @@ class ValidationManager:
"""Initialize the setup manager class.""" """Initialize the setup manager class."""
self.hacs = hacs self.hacs = hacs
self.hass = hass self.hass = hass
self._validatiors: dict[str, ActionValidationBase] = {} self._validators: dict[str, ActionValidationBase] = {}
@property @property
def validatiors(self) -> list[ActionValidationBase]: def validators(self) -> list[ActionValidationBase]:
"""Return all list of all tasks.""" """Return all list of all tasks."""
return list(self._validatiors.values()) return list(self._validators.values())
async def async_load(self, repository: HacsRepository) -> None: async def async_load(self, repository: HacsRepository) -> None:
"""Load all tasks.""" """Load all tasks."""
self._validatiors = {} self._validators = {}
validator_files = Path(__file__).parent validator_files = Path(__file__).parent
validator_modules = ( validator_modules = (
module.stem module.stem
@ -40,10 +40,10 @@ class ValidationManager:
if module.name not in ("base.py", "__init__.py", "manager.py") if module.name not in ("base.py", "__init__.py", "manager.py")
) )
async def _load_module(module: str): async def _load_module(module: str) -> None:
task_module = import_module(f"{__package__}.{module}") task_module = import_module(f"{__package__}.{module}")
if task := await task_module.async_setup_validator(repository=repository): if task := await task_module.async_setup_validator(repository=repository):
self._validatiors[task.slug] = task self._validators[task.slug] = task
await asyncio.gather(*[_load_module(task) for task in validator_modules]) await asyncio.gather(*[_load_module(task) for task in validator_modules])
@ -59,9 +59,9 @@ class ValidationManager:
and os.getenv("GITHUB_REPOSITORY") != repository.data.full_name and os.getenv("GITHUB_REPOSITORY") != repository.data.full_name
) )
validatiors = [ validators = [
validator validator
for validator in self.validatiors or [] for validator in self.validators or []
if ( if (
(not validator.categories or repository.data.category in validator.categories) (not validator.categories or repository.data.category in validator.categories)
and validator.slug not in os.getenv("INPUT_IGNORE", "").split(" ") and validator.slug not in os.getenv("INPUT_IGNORE", "").split(" ")
@ -69,10 +69,10 @@ class ValidationManager:
) )
] ]
await asyncio.gather(*[validator.execute_validation() for validator in validatiors]) await asyncio.gather(*[validator.execute_validation() for validator in validators])
total = len(validatiors) total = len(validators)
failed = len([x for x in validatiors if x.failed]) failed = len([x for x in validators if x.failed])
if failed != 0: if failed != 0:
repository.logger.error("%s %s/%s checks failed", repository.string, failed, total) repository.logger.error("%s %s/%s checks failed", repository.string, failed, total)

View File

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
from ..repositories.base import HacsRepository from typing import TYPE_CHECKING
from .base import ActionValidationBase, ValidationException from .base import ActionValidationBase, ValidationException
if TYPE_CHECKING:
from ..repositories.base import HacsRepository
async def async_setup_validator(repository: HacsRepository) -> Validator: async def async_setup_validator(repository: HacsRepository) -> Validator:
"""Set up this validator.""" """Set up this validator."""
@ -15,7 +19,7 @@ class Validator(ActionValidationBase):
more_info = "https://hacs.xyz/docs/publish/include#check-repository" more_info = "https://hacs.xyz/docs/publish/include#check-repository"
allow_fork = False allow_fork = False
async def async_validate(self): async def async_validate(self) -> None:
"""Validate the repository.""" """Validate the repository."""
if not self.repository.data.topics: if not self.repository.data.topics:
raise ValidationException("The repository has no valid topics") raise ValidationException("The repository has no valid topics")

View File

@ -1,4 +1,5 @@
"""Register_commands.""" """Register_commands."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -24,6 +25,7 @@ from .repository import (
hacs_repository_info, hacs_repository_info,
hacs_repository_refresh, hacs_repository_refresh,
hacs_repository_release_notes, hacs_repository_release_notes,
hacs_repository_releases,
hacs_repository_remove, hacs_repository_remove,
hacs_repository_state, hacs_repository_state,
hacs_repository_version, hacs_repository_version,
@ -57,6 +59,7 @@ def async_register_websocket_commands(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, hacs_repositories_clear_new) websocket_api.async_register_command(hass, hacs_repositories_clear_new)
websocket_api.async_register_command(hass, hacs_repositories_removed) websocket_api.async_register_command(hass, hacs_repositories_removed)
websocket_api.async_register_command(hass, hacs_repositories_remove) websocket_api.async_register_command(hass, hacs_repositories_remove)
websocket_api.async_register_command(hass, hacs_repository_releases)
@websocket_api.websocket_command( @websocket_api.websocket_command(
@ -75,7 +78,7 @@ async def hacs_subscribe(
"""Handle websocket subscriptions.""" """Handle websocket subscriptions."""
@callback @callback
def forward_messages(data: dict | None = None): def forward_messages(data: dict | None = None) -> None:
"""Forward events to websocket.""" """Forward events to websocket."""
connection.send_message(websocket_api.event_message(msg["id"], data)) connection.send_message(websocket_api.event_message(msg["id"], data))
@ -110,7 +113,6 @@ async def hacs_info(
"debug": hacs.configuration.debug, "debug": hacs.configuration.debug,
"dev": hacs.configuration.dev, "dev": hacs.configuration.dev,
"disabled_reason": hacs.system.disabled_reason, "disabled_reason": hacs.system.disabled_reason,
"experimental": hacs.configuration.experimental,
"has_pending_tasks": hacs.queue.has_pending_tasks, "has_pending_tasks": hacs.queue.has_pending_tasks,
"lovelace_mode": hacs.core.lovelace_mode, "lovelace_mode": hacs.core.lovelace_mode,
"stage": hacs.stage, "stage": hacs.stage,

View File

@ -1,15 +1,18 @@
"""Register info websocket commands.""" """Register info websocket commands."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
from ..utils.store import async_load_from_store, async_save_to_store from ..utils.store import async_load_from_store, async_save_to_store
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
@ -22,7 +25,7 @@ async def hacs_critical_list(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""List critical repositories.""" """List critical repositories."""
connection.send_message( connection.send_message(
websocket_api.result_message( websocket_api.result_message(
@ -44,7 +47,7 @@ async def hacs_critical_acknowledge(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Acknowledge critical repository.""" """Acknowledge critical repository."""
repository = msg["repository"] repository = msg["repository"]

View File

@ -1,11 +1,11 @@
"""Register info websocket commands.""" """Register info websocket commands."""
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
@ -15,6 +15,8 @@ from ..const import DOMAIN
from ..enums import HacsDispatchEvent from ..enums import HacsDispatchEvent
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from ..base import HacsBase from ..base import HacsBase
@ -30,7 +32,7 @@ async def hacs_repositories_list(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""List repositories.""" """List repositories."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
connection.send_message( connection.send_message(
@ -68,7 +70,7 @@ async def hacs_repositories_list(
for repo in hacs.repositories.list_all for repo in hacs.repositories.list_all
if repo.data.category in msg.get("categories", hacs.common.categories) if repo.data.category in msg.get("categories", hacs.common.categories)
and not repo.ignored_by_country_configuration and not repo.ignored_by_country_configuration
and (not hacs.configuration.experimental or repo.data.last_fetched) and repo.data.last_fetched
], ],
) )
) )
@ -88,7 +90,7 @@ async def hacs_repositories_clear_new(
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Clear new repositories for spesific categories.""" """Clear new repositories for specific categories."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
if repo := msg.get("repository"): if repo := msg.get("repository"):
@ -119,7 +121,7 @@ async def hacs_repositories_removed(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Get information about removed repositories.""" """Get information about removed repositories."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
content = [] content = []
@ -142,7 +144,7 @@ async def hacs_repositories_add(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Add custom repositoriy.""" """Add custom repositoriy."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = regex.extract_repository_from_url(msg["repository"]) repository = regex.extract_repository_from_url(msg["repository"])
@ -203,7 +205,7 @@ async def hacs_repositories_remove(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Remove custom repositoriy.""" """Remove custom repositoriy."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])

View File

@ -1,18 +1,21 @@
"""Register info websocket commands.""" """Register info websocket commands."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
from ..const import DOMAIN from ..const import DOMAIN
from ..enums import HacsDispatchEvent from ..enums import HacsDispatchEvent
from ..exceptions import HacsException
from ..utils.version import version_left_higher_then_right from ..utils.version import version_left_higher_then_right
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from ..base import HacsBase from ..base import HacsBase
@ -107,7 +110,7 @@ async def hacs_repository_ignore(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Ignore a repository.""" """Ignore a repository."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository_id = msg["repository"] repository_id = msg["repository"]
@ -140,7 +143,7 @@ async def hacs_repository_state(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Set the state of a repository""" """Set the state of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
@ -164,7 +167,7 @@ async def hacs_repository_version(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Set the version of a repository""" """Set the version of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
@ -194,7 +197,7 @@ async def hacs_repository_beta(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Show or hide beta versions of a repository""" """Show or hide beta versions of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
@ -221,24 +224,23 @@ async def hacs_repository_download(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Set the version of a repository""" """Set the version of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
try:
was_installed = repository.data.installed was_installed = repository.data.installed
if version := msg.get("version"): await repository.async_download_repository(ref=msg.get("version"))
repository.data.selected_tag = version
await repository.update_repository(force=True)
await repository.async_install()
repository.state = None
if not was_installed: if not was_installed:
hacs.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True}) hacs.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await hacs.async_recreate_entities() await hacs.async_recreate_entities()
await hacs.data.async_write() await hacs.data.async_write()
connection.send_message(websocket_api.result_message(msg["id"], {})) connection.send_message(websocket_api.result_message(msg["id"], {}))
except HacsException as exception:
repository.logger.error("%s %s", repository.string, exception)
connection.send_error(msg["id"], "error", str(exception))
@websocket_api.websocket_command( @websocket_api.websocket_command(
@ -253,7 +255,7 @@ async def hacs_repository_remove(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Remove a repository.""" """Remove a repository."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
@ -281,13 +283,15 @@ async def hacs_repository_refresh(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Refresh a repository.""" """Refresh a repository."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
await repository.update_repository(ignore_issues=True, force=True) await repository.update_repository(ignore_issues=True, force=True)
await hacs.data.async_write() await hacs.data.async_write()
# Update state of update entity
hacs.coordinators[repository.data.category].async_update_listeners()
connection.send_message(websocket_api.result_message(msg["id"], {})) connection.send_message(websocket_api.result_message(msg["id"], {}))
@ -304,7 +308,7 @@ async def hacs_repository_release_notes(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict[str, Any], msg: dict[str, Any],
): ) -> None:
"""Return release notes.""" """Return release notes."""
hacs: HacsBase = hass.data.get(DOMAIN) hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"]) repository = hacs.repositories.get_by_id(msg["repository"])
@ -324,3 +328,42 @@ async def hacs_repository_release_notes(
], ],
) )
) )
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repository/releases",
vol.Required("repository_id"): cv.string,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_repository_releases(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return releases."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository_id"])
try:
releases = await repository.async_get_releases()
except Exception as exception:
hacs.log.exception(exception)
connection.send_error(msg["id"], "unknown", str(exception))
return
connection.send_message(
websocket_api.result_message(
msg["id"],
[
{
"name": release.name,
"tag": release.tag_name,
"published_at": release.published_at,
"prerelease": release.prerelease,
}
for release in releases
],
)
)

View File

@ -1,7 +1,7 @@
{ {
"domain": "pid_controller", "domain": "pid_controller",
"name": "PID Controller", "name": "PID Controller",
"version": "v0.0.0", "version": "v1.1.5",
"documentation": "https://github.com/soloam/ha-pid-controller/", "documentation": "https://github.com/soloam/ha-pid-controller/",
"issue_tracker": "https://github.com/soloam/ha-pid-controller/issues", "issue_tracker": "https://github.com/soloam/ha-pid-controller/issues",
"dependencies": [], "dependencies": [],

View File

@ -18,7 +18,7 @@ class PIDController:
WARMUP_STAGE = 3 WARMUP_STAGE = 3
def __init__(self, P=0.2, I=0.0, D=0.0, logger=None): def __init__(self, P=2.7, I=37.6, D=0.0, logger=None):
self._logger = logger self._logger = logger
self._set_point = 0 self._set_point = 0
@ -92,7 +92,7 @@ class PIDController:
self._i_term = self.clamp_value(self._i_term, self._windup) self._i_term = self.clamp_value(self._i_term, self._windup)
# Calculate D # Calculate D
self._d_term = self._kd * delta_error / delta_time self._d_term = self._kd * delta_error
# Compute final output # Compute final output
self._output = self._p_term + self._i_term + self._d_term self._output = self._p_term + self._i_term + self._d_term

View File

@ -4,7 +4,9 @@ Connect two Home Assistant instances via the Websocket API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/remote_homeassistant/ https://home-assistant.io/components/remote_homeassistant/
""" """
from __future__ import annotations
import asyncio import asyncio
from typing import Optional
import copy import copy
import fnmatch import fnmatch
import inspect import inspect
@ -13,10 +15,15 @@ import re
from contextlib import suppress from contextlib import suppress
import aiohttp import aiohttp
from aiohttp import ClientWebSocketResponse
import homeassistant.components.websocket_api.auth as api import homeassistant.components.websocket_api.auth as api
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
from homeassistant.config import DATA_CUSTOMIZE try:
from homeassistant.core_config import DATA_CUSTOMIZE
except (ModuleNotFoundError, ImportError):
# hass 2024.10 or older
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (CONF_ABOVE, CONF_ACCESS_TOKEN, CONF_BELOW, from homeassistant.const import (CONF_ABOVE, CONF_ACCESS_TOKEN, CONF_BELOW,
CONF_DOMAINS, CONF_ENTITIES, CONF_ENTITY_ID, CONF_DOMAINS, CONF_ENTITIES, CONF_ENTITY_ID,
@ -28,10 +35,12 @@ from homeassistant.const import (CONF_ABOVE, CONF_ACCESS_TOKEN, CONF_BELOW,
from homeassistant.core import (Context, EventOrigin, HomeAssistant, callback, from homeassistant.core import (Context, EventOrigin, HomeAssistant, callback,
split_entity_id) split_entity_id)
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from custom_components.remote_homeassistant.views import DiscoveryInfoView from custom_components.remote_homeassistant.views import DiscoveryInfoView
@ -52,6 +61,7 @@ CONF_INSTANCES = "instances"
CONF_SECURE = "secure" CONF_SECURE = "secure"
CONF_SUBSCRIBE_EVENTS = "subscribe_events" CONF_SUBSCRIBE_EVENTS = "subscribe_events"
CONF_ENTITY_PREFIX = "entity_prefix" CONF_ENTITY_PREFIX = "entity_prefix"
CONF_ENTITY_FRIENDLY_NAME_PREFIX = "entity_friendly_name_prefix"
CONF_FILTER = "filter" CONF_FILTER = "filter"
CONF_MAX_MSG_SIZE = "max_message_size" CONF_MAX_MSG_SIZE = "max_message_size"
@ -64,6 +74,7 @@ STATE_RECONNECTING = "reconnecting"
STATE_DISCONNECTED = "disconnected" STATE_DISCONNECTED = "disconnected"
DEFAULT_ENTITY_PREFIX = "" DEFAULT_ENTITY_PREFIX = ""
DEFAULT_ENTITY_FRIENDLY_NAME_PREFIX = ""
INSTANCES_SCHEMA = vol.Schema( INSTANCES_SCHEMA = vol.Schema(
{ {
@ -103,7 +114,10 @@ INSTANCES_SCHEMA = vol.Schema(
], ],
), ),
vol.Optional(CONF_SUBSCRIBE_EVENTS): cv.ensure_list, vol.Optional(CONF_SUBSCRIBE_EVENTS): cv.ensure_list,
vol.Optional(CONF_ENTITY_PREFIX, default=DEFAULT_ENTITY_PREFIX): cv.string, vol.Optional(CONF_ENTITY_PREFIX,
default=DEFAULT_ENTITY_PREFIX): cv.string,
vol.Optional(CONF_ENTITY_FRIENDLY_NAME_PREFIX,
default=DEFAULT_ENTITY_FRIENDLY_NAME_PREFIX): cv.string,
vol.Optional(CONF_LOAD_COMPONENTS): cv.ensure_list, vol.Optional(CONF_LOAD_COMPONENTS): cv.ensure_list,
vol.Required(CONF_SERVICE_PREFIX, default="remote_"): cv.string, vol.Required(CONF_SERVICE_PREFIX, default="remote_"): cv.string,
vol.Optional(CONF_SERVICES): cv.ensure_list, vol.Optional(CONF_SERVICES): cv.ensure_list,
@ -152,6 +166,7 @@ def async_yaml_to_config_entry(instance_conf):
CONF_FILTER, CONF_FILTER,
CONF_SUBSCRIBE_EVENTS, CONF_SUBSCRIBE_EVENTS,
CONF_ENTITY_PREFIX, CONF_ENTITY_PREFIX,
CONF_ENTITY_FRIENDLY_NAME_PREFIX,
CONF_LOAD_COMPONENTS, CONF_LOAD_COMPONENTS,
CONF_SERVICE_PREFIX, CONF_SERVICE_PREFIX,
CONF_SERVICES, CONF_SERVICES,
@ -182,11 +197,11 @@ async def _async_update_config_entry_if_from_yaml(hass, entries_by_id, conf):
hass.config_entries.async_update_entry(entry, data=data, options=options) hass.config_entries.async_update_entry(entry, data=data, options=options)
async def setup_remote_instance(hass: HomeAssistantType): async def setup_remote_instance(hass: HomeAssistant.core.HomeAssistant):
hass.http.register_view(DiscoveryInfoView()) hass.http.register_view(DiscoveryInfoView())
async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup(hass: HomeAssistant.core.HomeAssistant, config: ConfigType):
"""Set up the remote_homeassistant component.""" """Set up the remote_homeassistant component."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
@ -210,7 +225,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
hass.async_create_task(setup_remote_instance(hass)) hass.async_create_task(setup_remote_instance(hass))
hass.helpers.service.async_register_admin_service( async_register_admin_service(hass,
DOMAIN, DOMAIN,
SERVICE_RELOAD, SERVICE_RELOAD,
_handle_reload, _handle_reload,
@ -246,12 +261,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
for domain in entry.options.get(CONF_LOAD_COMPONENTS, []): for domain in entry.options.get(CONF_LOAD_COMPONENTS, []):
hass.async_create_task(async_setup_component(hass, domain, {})) hass.async_create_task(async_setup_component(hass, domain, {}))
await asyncio.gather( await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
*[
hass.config_entries.async_forward_entry_setup(entry, platform)
for platform in PLATFORMS
]
)
await remote.async_connect() await remote.async_connect()
hass.async_create_task(setup_components_and_platforms()) hass.async_create_task(setup_components_and_platforms())
@ -292,7 +302,7 @@ async def _update_listener(hass, config_entry):
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
class RemoteConnection(object): class RemoteConnection:
"""A Websocket connection to a remote home-assistant instance.""" """A Websocket connection to a remote home-assistant instance."""
def __init__(self, hass, config_entry): def __init__(self, hass, config_entry):
@ -302,7 +312,7 @@ class RemoteConnection(object):
self._secure = config_entry.data.get(CONF_SECURE, False) self._secure = config_entry.data.get(CONF_SECURE, False)
self._verify_ssl = config_entry.data.get(CONF_VERIFY_SSL, False) self._verify_ssl = config_entry.data.get(CONF_VERIFY_SSL, False)
self._access_token = config_entry.data.get(CONF_ACCESS_TOKEN) self._access_token = config_entry.data.get(CONF_ACCESS_TOKEN)
self._max_msg_size = config_entry.data.get(CONF_MAX_MSG_SIZE) self._max_msg_size = config_entry.data.get(CONF_MAX_MSG_SIZE, DEFAULT_MAX_MSG_SIZE)
# see homeassistant/components/influxdb/__init__.py # see homeassistant/components/influxdb/__init__.py
# for include/exclude logic # for include/exclude logic
@ -326,9 +336,12 @@ class RemoteConnection(object):
self._subscribe_events = set( self._subscribe_events = set(
config_entry.options.get(CONF_SUBSCRIBE_EVENTS, []) + INTERNALLY_USED_EVENTS config_entry.options.get(CONF_SUBSCRIBE_EVENTS, []) + INTERNALLY_USED_EVENTS
) )
self._entity_prefix = config_entry.options.get(CONF_ENTITY_PREFIX, "") self._entity_prefix = config_entry.options.get(
CONF_ENTITY_PREFIX, "")
self._entity_friendly_name_prefix = config_entry.options.get(
CONF_ENTITY_FRIENDLY_NAME_PREFIX, "")
self._connection = None self._connection : Optional[ClientWebSocketResponse] = None
self._heartbeat_task = None self._heartbeat_task = None
self._is_stopping = False self._is_stopping = False
self._entities = set() self._entities = set()
@ -349,6 +362,26 @@ class RemoteConnection(object):
return entity_id return entity_id
return entity_id return entity_id
def _prefixed_entity_friendly_name(self, entity_friendly_name):
if (self._entity_friendly_name_prefix
and entity_friendly_name.startswith(self._entity_friendly_name_prefix)
== False):
entity_friendly_name = (self._entity_friendly_name_prefix +
entity_friendly_name)
return entity_friendly_name
return entity_friendly_name
def _full_picture_url(self, url):
baseURL = "%s://%s:%s" % (
"https" if self._secure else "http",
self._entry.data[CONF_HOST],
self._entry.data[CONF_PORT],
)
if url.startswith(baseURL) == False:
url = baseURL + url
return url
return url
def set_connection_state(self, state): def set_connection_state(self, state):
"""Change current connection state.""" """Change current connection state."""
signal = f"remote_homeassistant_{self._entry.unique_id}" signal = f"remote_homeassistant_{self._entry.unique_id}"
@ -445,7 +478,7 @@ class RemoteConnection(object):
async def _heartbeat_loop(self): async def _heartbeat_loop(self):
"""Send periodic heartbeats to remote instance.""" """Send periodic heartbeats to remote instance."""
while not self._connection.closed: while self._connection is not None and not self._connection.closed:
await asyncio.sleep(HEARTBEAT_INTERVAL) await asyncio.sleep(HEARTBEAT_INTERVAL)
_LOGGER.debug("Sending ping") _LOGGER.debug("Sending ping")
@ -460,7 +493,7 @@ class RemoteConnection(object):
try: try:
await asyncio.wait_for(event.wait(), HEARTBEAT_TIMEOUT) await asyncio.wait_for(event.wait(), HEARTBEAT_TIMEOUT)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error("heartbeat failed") _LOGGER.warning("heartbeat failed")
# Schedule closing on event loop to avoid deadlock # Schedule closing on event loop to avoid deadlock
asyncio.ensure_future(self._connection.close()) asyncio.ensure_future(self._connection.close())
@ -478,9 +511,13 @@ class RemoteConnection(object):
self.__id += 1 self.__id += 1
return _id return _id
async def call(self, callback, message_type, **extra_args): async def call(self, handler, message_type, **extra_args) -> None:
if self._connection is None:
_LOGGER.error("No remote websocket connection")
return
_id = self._next_id() _id = self._next_id()
self._handlers[_id] = callback self._handlers[_id] = handler
try: try:
await self._connection.send_json( await self._connection.send_json(
{"id": _id, "type": message_type, **extra_args} {"id": _id, "type": message_type, **extra_args}
@ -511,7 +548,7 @@ class RemoteConnection(object):
asyncio.ensure_future(self.async_connect()) asyncio.ensure_future(self.async_connect())
async def _recv(self): async def _recv(self):
while not self._connection.closed: while self._connection is not None and not self._connection.closed:
try: try:
data = await self._connection.receive() data = await self._connection.receive()
except aiohttp.client_exceptions.ClientError as err: except aiohttp.client_exceptions.ClientError as err:
@ -552,13 +589,13 @@ class RemoteConnection(object):
elif message["type"] == api.TYPE_AUTH_REQUIRED: elif message["type"] == api.TYPE_AUTH_REQUIRED:
if self._access_token: if self._access_token:
data = {"type": api.TYPE_AUTH, "access_token": self._access_token} json_data = {"type": api.TYPE_AUTH, "access_token": self._access_token}
else: else:
_LOGGER.error("Access token required, but not provided") _LOGGER.error("Access token required, but not provided")
self.set_connection_state(STATE_AUTH_REQUIRED) self.set_connection_state(STATE_AUTH_REQUIRED)
return return
try: try:
await self._connection.send_json(data) await self._connection.send_json(json_data)
except Exception as err: except Exception as err:
_LOGGER.error("could not send data to remote connection: %s", err) _LOGGER.error("could not send data to remote connection: %s", err)
break break
@ -570,12 +607,12 @@ class RemoteConnection(object):
return return
else: else:
callback = self._handlers.get(message["id"]) handler = self._handlers.get(message["id"])
if callback is not None: if handler is not None:
if inspect.iscoroutinefunction(callback): if inspect.iscoroutinefunction(handler):
await callback(message) await handler(message)
else: else:
callback(message) handler(message)
await self._disconnected() await self._disconnected()
@ -583,8 +620,8 @@ class RemoteConnection(object):
async def forward_event(event): async def forward_event(event):
"""Send local event to remote instance. """Send local event to remote instance.
The affected entity_id has to origin from that remote instance, The affected entity_id has to originate from that remote instance,
otherwise the event is dicarded. otherwise the event is discarded.
""" """
event_data = event.data event_data = event.data
service_data = event_data["service_data"] service_data = event_data["service_data"]
@ -628,6 +665,9 @@ class RemoteConnection(object):
_LOGGER.debug("forward event: %s", data) _LOGGER.debug("forward event: %s", data)
if self._connection is None:
_LOGGER.error("There is no remote connecion to send send data to")
return
try: try:
await self._connection.send_json(data) await self._connection.send_json(data)
except Exception as err: except Exception as err:
@ -636,7 +676,7 @@ class RemoteConnection(object):
def state_changed(entity_id, state, attr): def state_changed(entity_id, state, attr):
"""Publish remote state change on local instance.""" """Publish remote state change on local instance."""
domain, object_id = split_entity_id(entity_id) domain, _object_id = split_entity_id(entity_id)
self._all_entity_names.add(entity_id) self._all_entity_names.add(entity_id)
@ -661,7 +701,7 @@ class RemoteConnection(object):
try: try:
if f[CONF_BELOW] and float(state) < f[CONF_BELOW]: if f[CONF_BELOW] and float(state) < f[CONF_BELOW]:
_LOGGER.info( _LOGGER.info(
"%s: ignoring state '%s', because " "below '%s'", "%s: ignoring state '%s', because below '%s'",
entity_id, entity_id,
state, state,
f[CONF_BELOW], f[CONF_BELOW],
@ -669,7 +709,7 @@ class RemoteConnection(object):
return return
if f[CONF_ABOVE] and float(state) > f[CONF_ABOVE]: if f[CONF_ABOVE] and float(state) > f[CONF_ABOVE]:
_LOGGER.info( _LOGGER.info(
"%s: ignoring state '%s', because " "above '%s'", "%s: ignoring state '%s', because above '%s'",
entity_id, entity_id,
state, state,
f[CONF_ABOVE], f[CONF_ABOVE],
@ -680,15 +720,32 @@ class RemoteConnection(object):
entity_id = self._prefixed_entity_id(entity_id) entity_id = self._prefixed_entity_id(entity_id)
# Add local unique id
domain, object_id = split_entity_id(entity_id)
attr['unique_id'] = f"{self._entry.unique_id[:16]}_{entity_id}"
entity_registry = er.async_get(self._hass)
entity_registry.async_get_or_create(
domain=domain,
platform='remote_homeassistant',
unique_id=attr['unique_id'],
suggested_object_id=object_id,
)
# Add local customization data # Add local customization data
if DATA_CUSTOMIZE in self._hass.data: if DATA_CUSTOMIZE in self._hass.data:
attr.update(self._hass.data[DATA_CUSTOMIZE].get(entity_id)) attr.update(self._hass.data[DATA_CUSTOMIZE].get(entity_id))
for attrId, value in attr.items():
if attrId == "friendly_name":
attr[attrId] = self._prefixed_entity_friendly_name(value)
if attrId == "entity_picture":
attr[attrId] = self._full_picture_url(value)
self._entities.add(entity_id) self._entities.add(entity_id)
self._hass.states.async_set(entity_id, state, attr) self._hass.states.async_set(entity_id, state, attr)
def fire_event(message): def fire_event(message):
"""Publish remove event on local instance.""" """Publish remote event on local instance."""
if message["type"] == "result": if message["type"] == "result":
return return
@ -730,6 +787,11 @@ class RemoteConnection(object):
entity_id = entity["entity_id"] entity_id = entity["entity_id"]
state = entity["state"] state = entity["state"]
attributes = entity["attributes"] attributes = entity["attributes"]
for attr, value in attributes.items():
if attr == "friendly_name":
attributes[attr] = self._prefixed_entity_friendly_name(value)
if attr == "entity_picture":
attributes[attr] = self._full_picture_url(value)
state_changed(entity_id, state, attributes) state_changed(entity_id, state, attributes)

View File

@ -1,6 +1,8 @@
"""Config flow for Remote Home-Assistant integration.""" """Config flow for Remote Home-Assistant integration."""
from __future__ import annotations
import logging import logging
import enum import enum
from typing import Any, Mapping
from urllib.parse import urlparse from urllib.parse import urlparse
@ -16,6 +18,7 @@ from homeassistant.util import slugify
from . import async_yaml_to_config_entry from . import async_yaml_to_config_entry
from .const import (CONF_ENTITY_PREFIX, # pylint:disable=unused-import from .const import (CONF_ENTITY_PREFIX, # pylint:disable=unused-import
CONF_ENTITY_FRIENDLY_NAME_PREFIX,
CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, CONF_FILTER, CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, CONF_FILTER,
CONF_INCLUDE_DOMAINS, CONF_INCLUDE_ENTITIES, CONF_INCLUDE_DOMAINS, CONF_INCLUDE_ENTITIES,
CONF_LOAD_COMPONENTS, CONF_MAIN, CONF_OPTIONS, CONF_REMOTE, CONF_REMOTE_CONNECTION, CONF_LOAD_COMPONENTS, CONF_MAIN, CONF_OPTIONS, CONF_REMOTE, CONF_REMOTE_CONNECTION,
@ -31,11 +34,11 @@ ADD_NEW_EVENT = "add_new_event"
FILTER_OPTIONS = [CONF_ENTITY_ID, CONF_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW] FILTER_OPTIONS = [CONF_ENTITY_ID, CONF_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW]
def _filter_str(index, filter): def _filter_str(index, filter_conf: Mapping[str, str|float]):
entity_id = filter[CONF_ENTITY_ID] entity_id = filter_conf[CONF_ENTITY_ID]
unit = filter[CONF_UNIT_OF_MEASUREMENT] unit = filter_conf[CONF_UNIT_OF_MEASUREMENT]
above = filter[CONF_ABOVE] above = filter_conf[CONF_ABOVE]
below = filter[CONF_BELOW] below = filter_conf[CONF_BELOW]
return f"{index+1}. {entity_id}, unit: {unit}, above: {above}, below: {below}" return f"{index+1}. {entity_id}, unit: {unit}, above: {above}, below: {below}"
@ -50,8 +53,8 @@ async def validate_input(hass: core.HomeAssistant, conf):
conf[CONF_ACCESS_TOKEN], conf[CONF_ACCESS_TOKEN],
conf.get(CONF_VERIFY_SSL, False), conf.get(CONF_VERIFY_SSL, False),
) )
except OSError: except OSError as exc:
raise CannotConnect() raise CannotConnect() from exc
return {"title": info["location_name"], "uuid": info["uuid"]} return {"title": info["location_name"], "uuid": info["uuid"]}
@ -129,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input) return self.async_create_entry(title=info["title"], data=user_input)
user_input = user_input or dict() user_input = user_input or {}
host = user_input.get(CONF_HOST, self.prefill.get(CONF_HOST) or vol.UNDEFINED) host = user_input.get(CONF_HOST, self.prefill.get(CONF_HOST) or vol.UNDEFINED)
port = user_input.get(CONF_PORT, self.prefill.get(CONF_PORT) or vol.UNDEFINED) port = user_input.get(CONF_PORT, self.prefill.get(CONF_PORT) or vol.UNDEFINED)
secure = user_input.get(CONF_SECURE, self.prefill.get(CONF_SECURE) or vol.UNDEFINED) secure = user_input.get(CONF_SECURE, self.prefill.get(CONF_SECURE) or vol.UNDEFINED)
@ -149,10 +152,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_zeroconf(self, info): async def async_step_zeroconf(self, discovery_info):
"""Handle instance discovered via zeroconf.""" """Handle instance discovered via zeroconf."""
properties = info.properties properties = discovery_info.properties
port = info.port port = discovery_info.port
uuid = properties["uuid"] uuid = properties["uuid"]
await self.async_set_unique_id(uuid) await self.async_set_unique_id(uuid)
@ -203,11 +206,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry): def __init__(self, config_entry):
"""Initialize remote_homeassistant options flow.""" """Initialize remote_homeassistant options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self.filters = None self.filters : list[Any] | None = None
self.events = None self.events : set[Any] | None = None
self.options = None self.options : dict[str, Any] | None = None
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input : dict[str, str] | None = None):
"""Manage basic options.""" """Manage basic options."""
if self.config_entry.unique_id == REMOTE_ID: if self.config_entry.unique_id == REMOTE_ID:
return self.async_abort(reason="not_supported") return self.async_abort(reason="not_supported")
@ -235,6 +238,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
) )
}, },
): str, ): str,
vol.Optional(
CONF_ENTITY_FRIENDLY_NAME_PREFIX,
description={
"suggested_value": self.config_entry.options.get(
CONF_ENTITY_FRIENDLY_NAME_PREFIX
)
},
): str,
vol.Optional( vol.Optional(
CONF_LOAD_COMPONENTS, CONF_LOAD_COMPONENTS,
default=self._default(CONF_LOAD_COMPONENTS), default=self._default(CONF_LOAD_COMPONENTS),
@ -252,7 +263,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_domain_entity_filters(self, user_input=None): async def async_step_domain_entity_filters(self, user_input=None):
"""Manage domain and entity filters.""" """Manage domain and entity filters."""
if user_input is not None: if self.options is not None and user_input is not None:
self.options.update(user_input) self.options.update(user_input)
return await self.async_step_general_filters() return await self.async_step_general_filters()
@ -289,21 +300,25 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
# Each filter string is prefixed with a number (index in self.filter+1). # Each filter string is prefixed with a number (index in self.filter+1).
# Extract all of them and build the final filter list. # Extract all of them and build the final filter list.
selected_indices = [ selected_indices = [
int(filter.split(".")[0]) - 1 int(filterItem.split(".")[0]) - 1
for filter in user_input.get(CONF_FILTER, []) for filterItem in user_input.get(CONF_FILTER, [])
] ]
self.options[CONF_FILTER] = [self.filters[i] for i in selected_indices] if self.options is not None:
self.options[CONF_FILTER] = [self.filters[i] for i in selected_indices] # type: ignore
return await self.async_step_events() return await self.async_step_events()
selected = user_input.get(CONF_FILTER, []) selected = user_input.get(CONF_FILTER, [])
new_filter = {conf: user_input.get(conf) for conf in FILTER_OPTIONS} new_filter = {conf: user_input.get(conf) for conf in FILTER_OPTIONS}
selected.append(_filter_str(len(self.filters), new_filter))
self.filters.append(new_filter) selected.append(_filter_str(len(self.filters), new_filter)) # type: ignore
self.filters.append(new_filter) # type: ignore
else: else:
self.filters = self.config_entry.options.get(CONF_FILTER, []) self.filters = self.config_entry.options.get(CONF_FILTER, [])
selected = [_filter_str(i, filter) for i, filter in enumerate(self.filters)] selected = [_filter_str(i, filterItem) for i, filterItem in enumerate(self.filters)] # type: ignore
strings = [_filter_str(i, filter) for i, filter in enumerate(self.filters)] if self.filters is None:
self.filters = []
strings = [_filter_str(i, filterItem) for i, filterItem in enumerate(self.filters)]
return self.async_show_form( return self.async_show_form(
step_id="general_filters", step_id="general_filters",
data_schema=vol.Schema( data_schema=vol.Schema(
@ -322,13 +337,15 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_events(self, user_input=None): async def async_step_events(self, user_input=None):
"""Manage event options.""" """Manage event options."""
if user_input is not None: if user_input is not None:
if ADD_NEW_EVENT not in user_input: if ADD_NEW_EVENT not in user_input and self.options is not None:
self.options[CONF_SUBSCRIBE_EVENTS] = user_input.get( self.options[CONF_SUBSCRIBE_EVENTS] = user_input.get(
CONF_SUBSCRIBE_EVENTS, [] CONF_SUBSCRIBE_EVENTS, []
) )
return self.async_create_entry(title="", data=self.options) return self.async_create_entry(title="", data=self.options)
selected = user_input.get(CONF_SUBSCRIBE_EVENTS, []) selected = user_input.get(CONF_SUBSCRIBE_EVENTS, [])
if self.events is None:
self.events = set()
self.events.add(user_input[ADD_NEW_EVENT]) self.events.add(user_input[ADD_NEW_EVENT])
selected.append(user_input[ADD_NEW_EVENT]) selected.append(user_input[ADD_NEW_EVENT])
else: else:

Some files were not shown because too many files have changed in this diff Show More